The Problem

Surely and like me, you are trying to be more secure when connecting Jenkins with your AWS Accounts assuming a role. If you are asking What is that? , please read this: https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html

Of course, there are many different options to use, but the problem always surrounds us, if you use a plugin then the maintainability and security when talking about Jenkins plugins for sure decrease.

I particularly hate Jenkins, from my point of view this is an obsolete tool trying to survive in the modern world, and if you are concerned about security (and maintainability) sure understand my point.

So, why I’m writing about that?

  • Because unfortunately I still using Jenkins and sweating their maintenance
  • Because as a rule of thumb I try to avoid plugins that don’t have any release in the time windows of 6-12 months
  • Helps others to avoid loose time and security when needs the same that me, AWS cross-account connections using Jenkins assuming a role
  • Because at least if I have an Issue this is my code and I can fix it

What’s this?

  • This is a guide and code for somebody using Jenkins shared library
  • This is a minimal blog entry to help someone that understands Jenkins and Groovy
  • This could help you if you are using Jenkins + Jenkins shared library + AWS cross-account and cross-region roles

what it is not?

  • A tutorial
  • A very well-explained and step-by-step guide
  • Something you surely need to use
  • An AWS cross-account tutorial or explanation guide

The Solution

This is how looks a segment of the code on my production Jenkins declarative pipeline.

Look at withAwsEnVars (lines: 4, 11) pipeline tags

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
...
      stage('setup repositories') {
        steps {
          withAwsEnVars(roleName: cicd.getRole(), roleAccount: codeArtifact.getOwner('npm-private')) {
            script {
              log.info('setting repositories \'npm-private\' credentials for dependencies')
              codeArtifact.setupNpmrc('npm-private', '@my-company-namespace', params.timeoutTime*60)
              codeArtifact.setupNpmrc('npm-private', '@my-company-other-namespace', params.timeoutTime*60)
            }
          }
          withAwsEnVars(roleName: cicd.getRole(), roleAccount: codeArtifact.getOwner('npm-public')) {
            script {
              log.info('setting repositories \'npm-public\' credentials for dependencies')
              codeArtifact.setupNpmrc('npm-public', '', params.timeoutTime*60)
            }
          }
        }
      }
...

withAwsEnVars is a Groovy function used in my Jenkins Shared Library and this is the withAwsEnVars.groovy file content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/env groovy

/*
paramters:
  roleArn (required)
  roleAccount (required)
  sessionName (optional)
  sessionDuration (optional)

Examples:
  stage('test aws credential') {
    steps {
      withAwsEnVars(roleName:'cicd-execution-role', roleAccount: '12345678910') {
        sh "echo TOKEN: ${AWS_SESSION_TOKEN}"
        sh "echo KEY: ${AWS_SECRET_ACCESS_KEY}"
        sh "echo ID: ${AWS_ACCESS_KEY_ID}"
        sh 'aws s3 ls'
      }
      sh "exit 1"
    }
  }
*/
def call(Map params, Closure body) {

  if (!params.roleName) {
    error """
      parameter 'roleName' is required.
      ---
      Example:  withAwsEnVars(roleName:'cicd-execution-role', roleAccount: '12345678910') {...}
    """
  }

  if (!params.roleAccount) {
    error """
      parameter 'roleAccount' is required.
      ---
      Example:  withAwsEnVars(roleName:'cicd-execution-role', roleAccount: '12345678910') {...}
    """
  }

  // get optional parameters if not set default
  String sessionName = params.get('sessionName', 'jenkins')
  Integer duration = params.get('sessionDuration', 900)

  cred = awsCredentials.getFromAssumeRole(params.roleName, params.roleAccount, sessionName, duration)

  AWS_ACCESS_KEY_ID = cred.AccessKeyId
  AWS_SECRET_ACCESS_KEY = cred.SecretAccessKey
  AWS_SESSION_TOKEN = cred.SessionToken

  wrap([
      $class: 'MaskPasswordsBuildWrapper',
      varPasswordPairs: [
        [password: AWS_ACCESS_KEY_ID],
        [password: AWS_SECRET_ACCESS_KEY],
        [password: AWS_SESSION_TOKEN]
      ]
  ]) {
    withEnv([
      "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}",
      "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}",
      "AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}"
    ]) {
      body()
    }
  }
}

and this is my awsCredentials.groovy file content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env groovy

/*
Parameters:
  roleName (required)
  roleAccount (required)

Examples:
  cred = awsCredentials.getFromAssumeRole(...)
  accessKey = awsCredentials.getFromAssumeRole(...).AccessKeyId
*/
String getFromAssumeRole(String roleName, String roleAccount, String sessionName='jenkins', Integer duration=900){

  String roleArn = 'arn:aws:iam::' + roleAccount +':role/'+ roleName

  List<String> options = []

  options += "--role-arn ${roleArn}"
  options += "--role-session-name ${sessionName}"
  options += "--duration-seconds ${duration}"
  options += "--query 'Credentials'"

  optionsString = options.join(" ")

  // this is used to mask any critical information
  wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: [[password: roleArn], [password: sessionName]]]) {
    String strCreds = sh(
      returnStdout: true,
      script: """
        aws sts assume-role ${optionsString}
        """).trim()

    return readJSON(text: strCreds)
  }
}

from the code above, definitions are located in the Jenkins Shared Library

The minimal requirements on your Jenkins controller and agents

So, What is the Magic? Why do I say this is secure?

Maybe after looking at the following pipeline code, you will see how easy is to use this, and this is secure because if you execute the following code in your pipeline:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  ...
   stage('test aws credential') {
     steps {
       withAwsEnVars(roleName:'cicd-execution-role', roleAccount: '12345678910') {
         sh "echo TOKEN: ${AWS_SESSION_TOKEN}"
         sh "echo KEY: ${AWS_SECRET_ACCESS_KEY}"
         sh "echo ID: ${AWS_ACCESS_KEY_ID}"
         sh 'aws s3 ls'
       }
       sh "exit 1"
     }
   }
...

you will see masked the TOKEN, KEY and ID. Instead of seeing the real value, you will see *********** characters.

Tools and Concepts

There are various tools and concepts I used here, the first that allows me to do that so easily was Groovy Closures and it is explained how to use on Jenkins Shared Library –> Defining custom steps.

Then we have the tool withEnv provided by the Jenkins Plugin – Pipeline: Basic Steps and in combination with the use of the Groovy Closures allowed me to export the AWS Environment Variables coming from awsCredentials.getFromAssumeRole(…) groovy function into a container script part.

But, make sure that anyone who will use Jenkins Controller and Pipeline doesn’t have access to the values of the AWS Environment Variables is the job of wrap provided by the Jenkins Plugin – Pipeline: Basic Steps + maskPasswords provided by Jenkins Plugin – Mask Passwords.

Closing

Even hating Jenkins like me, you can find different ways to do your life easy and secure with him.

If you want to look at my GitHub repositories related to Jenkins, here you have: