Jenkins Pipelines Deployments with MFA

Deploying to production can now be controlled using MFA devices. The device can be shared between a team or individualised per user which allows for greater security controls in deployment pipelines.

The solution utilises AWS services IAM MFA to manage the MFA devices, IAM STS (Security Token Service) cross account roles to obtain credentials to the AWS account and SecretsManager with Lambda to store and rotate the AWS Access Keys.

Architecture Diagram

To implement into a pipeline we need to set up some IAM users. This can be achieved through ciinabox 2 by adding a list of users to the mfa: section. Commit the change and wait for the update.

mfa:
  users:
    - name: demo-user-a
      roles:
        # Role in the production account we want to assume
        - arn:aws:iam::12345678912:role/bearse/feature/iam/ciinabox-v2
    - name: demo-user-b
      roles:
        - arn:aws:iam::12345678912:role/bearse/feature/iam/ciinabox-v2

Once updated, the Bearse IAM feature needs to be updated with the arns of the new users to grant them trusted authority to assume role in the production account.

CiinaboxMFAUsers - a comma delimited string of IAM user arns

Now the setup has been complete we can implement this in our Jenkins Pipeline. At the beginning of the stage we need an input step with 2 parameters, the first being a drop down list of approved mfa devices and a string input for the MFA token. The drop down list is only required if multiple MFA devices are being used.

input {
  message "Input MFA code"
  parameters {
    choice(name: 'MFA_ID', choices: ['demo-user-a','demo-user-b'], description: 'MFA user')
    string(name: 'MFA_INPUT', defaultValue: '000000', description: 'MFA code')
  }
}

Then in our step we need to call the withMFA() function to assume the role in the production account using the MFA device and token. Inside the block we can then call our cloudformation() method utilising the STS credentials.

stage('deploy production') {
  input {
    message "Input MFA code"
    parameters {
      choice(name: 'MFA_ID', choices: ['demo-user-a','demo-user-b'], description: 'MFA user')
      string(name: 'MFA_INPUT', defaultValue: '000000', description: 'MFA code')
    }
  }
  environment {
    STACK_NAME = 'demo-stack-prod'
    ENVIRONMET_NAME = 'prod'
  }
  steps {
    withMFA(
      credentials: "/reference/jenkins/mfa/${env.MFA_ID}",
      role: env.ASSUME_ROLE,
      region: env.AWS_REGION,
      accountId: env.PROD_ACCOUNT,
      mfaToken: env.MFA_INPUT,
      mfaId: env.MFA_ID
    ) {
      cloudformation(
        action: 'create',
        region: env.AWS_REGION,
        stackName: env.STACK_NAME,
        templateUrl: env.TEMPLATE_URL,
        parameters: [
          'EnvironmentName': env.ENVIRONMET_NAME,
          'Test': 'test'
        ],
        tags: [
          'CreatedBy': 'gus'
        ],
        roleArn: env.CFN_ROLE
      )
    }
  }
}