Forrest Brazeal in security 6 minutes to read

Enforcing the 'Two-Person Rule' with AWS CodePipeline

You might be alive today thanks to the “two-person rule” - it’s the US military policy that requires two keys to launch a nuclear missile, so one crazy officer can’t start World War III.

Maybe your business’s software isn’t quite as dangerous as a nuclear warhead, but you’d still prefer it didn’t blow up in your face. For that reason, you might choose to integrate some form of the two-person rule as part of your CI/CD pipeline: requiring multiple people to sign off manually on a code change before it enters your production environment.

I’ve been working recently with CodePipeline, an AWS service that automates code builds and deployments in the cloud. When I saw that CodePipeline supports manual approval actions, I thought: “How hard could it be to implement the two-person rule as part of a pipeline stage?”

Implementing Multiple Approvers in AWS CodePipeline

Since we’re using IAM authentication for users in our AWS environment, a two-person approval solution should meet the following criteria:

  • CodePipeline must record two or more manual approvals before successfully exiting a pipeline stage
  • Each approval must be associated with a valid IAM user or role
  • The approvers must be different: the same IAM principal should not be able to provide more than one approval for the execution of the same pipeline stage

IAM Limitations

The most logical way to set this up would be to place two approval actions in parallel at the end of the pipeline stage, looking something like this:

CodePipeline stage with two approval actions

You could then create two IAM roles or groups and give each group permissions to approve only one of the two approval actions. Now you can just divide your administrative users among the two groups and you’re good to go.

Unfortunately, AWS CodePipeline does not currently support resource-level permissions for the “PutApprovalResult” IAM action. This means there is no way to limit an IAM principal to approving a single action, and we can’t use IAM by itself to enforce the two-person rule. At least for now, we’ll have to keep looking for a solution.

Lambda Invocation

Hey, look - AWS lets you hook a Lambda function into a CodePipeline stage. This feature would normally be used to deploy code or run tests, but nothing prevents us from writing a function that introspects on the state of the pipeline itself.

Essentially, we just want to be sure that two different IAM entities performed our two approval actions, and throw an error if they don’t match. This is a relatively easy check using the GetPipelineStage API call as shown in the example Python Lambda function below:

from __future__ import print_function
import boto3
import traceback

code_pipeline = boto3.client('codepipeline')

def put_job_success(job, message):
def put_job_failure(job, message):
    code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'})

def lambda_handler(event, context):
        job_id = event['CodePipeline.job']['id']
        state = code_pipeline.get_pipeline_state(name='YOUR_PIPELINE_NAME_HERE')
        stage = state['stageStates'][1]
        first_approver = stage['actionStates'][0]['latestExecution']['lastUpdatedBy']
        second_approver = stage['actionStates'][1]['latestExecution']['lastUpdatedBy']

        if first_approver == second_approver:
            put_job_failure(job_id, "ERROR: This pipeline does not permit the same approver ({}) to provide two approvals!".format(first_approver))
            put_job_success(job_id, "Two different approvers signed off on this change: {} , {}".format(first_approver, second_approver))

    except Exception as e:
        # If any other exceptions which we didn't expect are raised
        # then fail the job and log the exception message.
        put_job_failure(job_id, 'Function exception: ' + str(e))


The example implementation above would only be guaranteed to enforce unique approvals between IAM users. It might require some administrative effort on your part to ensure that roles with permissions to approve CodePipeline executions are associated with distinct sets of users. You would also have to think about sessions - you would not want a user to assume the same role twice using different session names and have your code recognize that as two different roles. These considerations are left as an exercise for the reader. :)

Now we can chain this function into our pipeline. As long as the approvers are different, the pipeline will succeed as shown below:

Successful CodePipeline execution with two approval actions

If the same user tries to approve both manual actions, the pipeline execution will fail with an error like this:

Denied CodePipeline execution with two identical approval actions

Further Limitations

We now have a solution that fulfills our primary goal: stopping a CodePipeline execution unless it gets manual approvals from two distinct identities. However, it’s still not an ideal solution. Failing the whole pipeline execution just because somebody tried to approve both actions is a bit drastic - after all, especially if you’re spinning up a bunch of CodeBuild jobs, a CodePipeline run can take quite a bit of time. It would be much better if we could just deny the second approval attempt and keep that approval action open for review by another authorized person, essentially pausing the execution instead of outright failing it.

CodePipeline does support an API call to retry a particular stage, but as of now it only retries failed actions in that stage. There’s no way to arbitrarily mark a “successful” action as failed from a later point in the pipeline execution, or vice versa - which is probably a good thing - but it does mean that we can’t use our Lambda function to reach back and mark that second approval action as failed so we can retry it.

Hopefully CodePipeline releases some IAM updates soon that will make this process smoother! We’ll be sure to update this post if and when that happens.