
Have we given up on the least privileged principle? Personally, I am a big fan of it. But let’s be honest, it can also be tough to follow the principle strictly. With the rise of CDK, it became even harder.
Why does CDK make it harder?
CDK is a great tool to use when you are developing your infrastructure. You can easily build resources using the level 1 and level 2 constructs. So far, so good. The problem lies within the level 2 constructs. They are somewhat opinionated about how you should use them. For example, when you want to store secrets in the Cloud, you create a secret in Secret Manager. This secret needs to be encrypted, so we will use KMS. This can easily be achieved through CDK:
const secret = new secretsmanager.Secret(this, 'Secret', {
secretName: `MySecret`,
description: 'My super Secret',
encryptionKey: kmsKey,
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'myUser' }),
generateStringKey: 'password',
},
});
As you can see in the code example. We are using a customer-managed KMS key. We do this because we want to control who can use the key for decryption. The fact that you pass the key into the Secret construct means that CDK will help you and adds a policy to your KMS key. The policy that gets added is the following:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:root"
},
"Action": [
"kms:CreateGrant",
"kms:Decrypt",
"kms:DescribeKey",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.eu-west-1.amazonaws.com"
}
}
}
Those familiar with KMS policies will notice that any principal can now use the key if it is used through the secrets manager service. At first, you might think it is not that bad or convenient. But the problem with this is that any principal with an allow statement on the secretsmanager:GetSecretValue
action will be able to read your secret.
It gets worse
You are storing the secret for a reason. You probably need to read it somewhere in your workload. To do that, you need to create a role and grant the role the rights to read the secret. This is easily done with CDK.
secret.grantRead(role)
But this simple statement again changes the KMS policy. It will add the following policy.
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/MyRole"
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.eu-west-1.amazonaws.com"
}
}
}
At least it’s specific to the role, and this is what you want. But remember the previous policy? It already allows all principals to use this key.
How to work around this?
Sadly, the only way around this is to use the level 1 constructs. These constructs are not opinionated and are a one-to-one mapping to the CloudFromation resources. With those level 1 constructs, you can specify the KMS key without changing the key policy for you. The flip side is that you do need to allow all principals who need to read the secret in your KMS key policy.
Conclusion
In summary, while CDK greatly simplifies infrastructure development, it can unintentionally weaken strict least privilege practices, especially around KMS key policies. You may need to step down to level 1 constructs to maintain tighter control, trading some convenience for the precision and security your workloads deserve.
Photo by AS Photography