Implementing a Double-Lock for IAM Role Switching

IAM provides a way for users and roles to become another role. This is known as IAM role switching and uses the underlying sts:AssumeRole action. You can restrict IAM role switching in one of two ways, what I like to call the single lock and double lock methods.

With any IAM role switch, there involves a two-way handshake. The person (source) switching to the role (target) must be allowed to assume the role, plus the target must allow the source to assume it. That way, an IAM role switch can be used to switch between roles within the same account, or roles within different AWS account (maybe one that you don’t even own).

The target IAM role’s trust policy typically looks like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::01234567890:root"
      },
      "Action": "sts:AssumeRole"
      }
    }
  ]
}

The Principal element here is key; you can set it to a specific user, given by their username, but that opens a username takeover vulnerability if a user created with the same username as someone has used to have access to a specific account. As you’ll see in the Double Lock section, the use of IAM user IDs is preferred over usernames.

If you set the Principal to AWS: *, you have what is known as an IAM backdoor vulnerability (neé AssumeTheWorst), allowing anyone in AWS into your account.

Single Lock

The single lock method involves allowing an IAM user, group or role to assume another role, be in the same account or another account. This involves creating an IAM policy

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "sts:AssumeRole",
    "Resource": [
      “aws:aws:iam::01234567890:role/SOME/ROLENAME/HERE”
    ]
  }
}

the problem with the single lock is that the access control is primarily happening on the caller’s side, which the target role cannot trust. Since the target role trusts anyone from the source account, there is no further access control before allowing the caller to assume the role. Anyone who has administrative privileges in the source account and known the ARN of the target role can assume it. But this can be prevented by using a double-lock.

Double Lock

A double-lock is like the IAM form of a man trap. It entails verifying the access control on both the caller’s and the target’s side before allowing the account to be assumed. The trust policy looks basically the same, except for some Conditions added that verify the caller’s identity.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::01234567890:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:userid": [
            "AIDAIREDACTEDNI5XNVM"
          ]
        }
      }
    }
  ]
}

Note that IAM user IDs are used instead of usernames. This removes the username takeover vulnerability previously mentioned. IAM user IDs are not displayed in the AWS Web console, but they exist on every user and appear to be random strings. Whether they are random or not doesn’t matter, the important point is that they are a value that is determined by AWS and not by a customer, whereas a username is determined by a customer. Security best practices state that a receiver should never trust information provided by a caller.

Lastly, I strongly recommend the use of MFA checks on these types of IAM roles. It adds an extra layer of protection for those users who use the AWS CLI. This is enabled through the use of a Condition statement, as seen below.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::01234567890:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:userid": [
            "AIDAIREDACTEDNI5XNVM"
          ]
        },
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}