Cloud people!
In this opportunity I want to share an example of as covering a scenario of temporary access in policies using condition of date and time.
This is useful in cases where it is necessary to use an action within an AWS policy to do something within a certain time and subsequently lose this access for security reasons.
The case is following:
An application need a user specific, this user is created during execution time and only users that have permissions to retrieve secrets in secrets manager can be authenticated inside bastion.
In addition, any other user who has permissions to connect to the bastion from Systems Manager and does not have permissions to retrieve the user's secret from secrets manager will not be able to perform actions even if they try to retrieve the secret from the bastion, as there is only a 5-minute gate to retrieve the secret from the instance profile, these 5 minutes are enough for the bootstrap process.
Requirements
Let's see how we can do this using terraform.
Reference Architecture
This is the link for all code terraform in branch sysops1.
Step 1.
Adding the kms-module allows the creation of KMS to encrypt resource.
module "kms" {
source = "terraform-aws-modules/kms/aws"
version = "2.1.0"
deletion_window_in_days = 7
description = "ec2 key for testing"
enable_key_rotation = false
is_enabled = true
key_usage = "ENCRYPT_DECRYPT"
multi_region = false
aliases = ["ec2/testing"]
}
Step 2.
Adding the ec2-module allows the creation of ec2 instance.
This is the code for deploy ec2 instance that is used as Bastion, in this step the kms arn is used to encrypt volumes EBS, remember that this a good practice.
module "ec2-instance" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "5.6.0"
name = "bastion"
ami = "ami-023c11a32b0207432"
instance_type = "t3.medium"
subnet_id = "subnet-0241994880cd395eb"
vpc_security_group_ids = ["sg-0ea884b195d4fd56b"]
associate_public_ip_address = true
availability_zone = ""
iam_instance_profile = aws_iam_instance_profile.test_profile.name
user_data = templatefile("${path.module}/userdatafile/scripts_bastion/cloud_init.yaml", { user0 = local.users[0], secret_name0 = aws_secretsmanager_secret.userlinux-pass.name })
user_data_replace_on_change = true
hibernation = true
enable_volume_tags = true
root_block_device = [
{
encrypted = true
kms_key_id = module.kms.key_id
volume_type = "gp3"
throughput = 200
volume_size = 20
},
]
ebs_block_device = [
{
device_name = "/dev/sdf"
volume_type = "gp3"
volume_size = 20
throughput = 200
encrypted = true
encrypted = true
kms_key_id = module.kms.key_id
}
]
}
In this case a terraform templatefile function is used to retrieve a value from terraform and pass it to cloud-init.yaml which is then used for the bootstrap process.
#cloud-config
package_update: true
packages:
- unzip
write_files:
- path: /tmp/install_bastion_userdata.sh
content: |
#!/bin/bash
#versiones
awscli_version=2.13.32
cd /tmp
#Instalando AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-$awscli_version.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
#Instalación de SSM
sudo yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl enable amazon-ssm-agent
sudo systemctl start amazon-ssm-agent
#Instalando jq
sudo dnf install jq -y
permissions: '0755'
runcmd:
- /tmp/install_bastion_userdata.sh
- GROUPNAME="SysOps" #Creación de un grupo en Linux.
- GROUP_UID=11001 #Creación de un grupo en Linux
- USER0_UID=10001
- groupadd -g $GROUP_UID $GROUPNAME #Creación de un grupo en Linux
- /usr/local/bin/aws secretsmanager get-secret-value --secret-id ${secret_name0} --query SecretString --output text > /tmp/secreto.txt --region us-east-1 #user
- PASSWORD=$(jq -r '.password' /tmp/secreto.txt)
- useradd -u $USER0_UID -m -d /home/${user0} -s /bin/bash -g $GROUPNAME ${user0}
- echo "${user0}:$PASSWORD" | sudo chpasswd
- rm -f /etc/sudoers.d/90-cloud-init-users && rm -f /etc/sudoers.d/ssm-agent-users && rm -f /tmp/secreto.txt
- echo "${user0} ALL=(ALL) ALL" >> /etc/sudoers
- rm -f /etc/sudoers.d/90-cloud-init-users ssm-agent-users #Remove ssm-user from sudoers
- cd /etc/sudoers.d
- echo "#User rules for ssm-user" > ssm-agent-users
Step 3.
This step is important because it allows the bastion instance to have only 5 minutes to retrieve the secret that is then used to set the password of the user that is created during the runtime, in which the instance is parameterized using userdata.
resource "aws_iam_policy" "custom_policy" {
name = "segoja7-policy-testing"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : [
"secretsmanager:GetSecretValue",
],
"Resource" : [aws_secretsmanager_secret_version.userlinux-pass-val.arn],
"Effect" : "Allow"
"Condition" : {
"DateGreaterThan" : { "aws:CurrentTime" :timestamp() },
"DateLessThan" : { "aws:CurrentTime" : timeadd(timestamp(), "5m") }
}
},
{
"Effect" : "Allow",
"Action" : [
"kms:Decrypt",
"kms:DescribeKey",
"kms:GenerateDataKey"
],
"Resource" : module.kms.key_arn
}
]
})
}
In this step, it is creating a police with permissions to retrieve secrets in a resource specific. Additional, as the secret is encrypted, are necessary permissions about KMS.
Check that first statement have conditions and is using two functions of terraform that handle date and time.
For more information, check these links for function timestamp and timeadd.
And this link for AWS conditions policy
Step 4.
In this step, a role is being created with permissions so that the ec2 service can assume the role, in this case the bastion instance.
resource "aws_iam_role" "custom_role" {
name = "custom-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
"Sid" : "EC2AssumeRole",
"Effect" : "Allow",
"Principal" : {
"Service" : "ec2.amazonaws.com"
},
"Action" : "sts:AssumeRole"
},
]
})
}
Step 5.
In this step, two policies are attached to the role, the custom policy that was created in step 3 and a managed policy that allows to connect via systems manager.
resource "aws_iam_role_policy_attachment" "policy-attachment" {
for_each = {
"AmazonSSMManagedInstanceCore" = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
"CustomPolicy" = aws_iam_policy.custom_policy.arn,
}
policy_arn = each.value
role = aws_iam_role.custom_role.name
}
resource "aws_iam_instance_profile" "instance_profile" {
name = "custom-profile"
role = aws_iam_role.custom_role.name
}
Step 6.
In this step, it is created the secret using a randon_password resource of terraform.
resource "aws_secretsmanager_secret" "userlinux-pass" {
name = "secrets/bastion/${local.users[0]}"
description = "password secret for user: ${local.users[0]}"
recovery_window_in_days = 0
kms_key_id = module.kms.key_id
}
resource "aws_secretsmanager_secret_version" "userlinux-pass-val" {
secret_id = aws_secretsmanager_secret.userlinux-pass.id
secret_string = jsonencode({
username = local.users[0]
password = random_password.user_password.result
})
}
resource "random_password" "user_password" {
length = 16
special = true
}
For this moment, all code of main.tf is completed and now is possible run terraform apply.
terraform apply --auto-approve #use with precaution
Step 7.
Check the resources that were created.
An Ec2 bastion with an instance profile.
Using cloud-init.yaml this is a part of the userdata result.
A Role with permission policies.
The final result of the policy is a dynamic date and time value and it is only possible to retrieve the secret in the first 5 minutes after the policy was created.
A secret with password for the user created with cloud-init.yaml.
Step 8.
Checking that policy is functional and it is not posible retrieve the secret from ec2 bastion using the instance profile.
verifying that instance profile is work connecting from System Manager.
aws sts get-caller-identity
try retrieve the secret.
aws secretsmanager get-secret-value --secret-id secrets/bastion/segoja7 --query SecretString --region us-east-1
Step 9.
connecting with user created in execution time using userdata.
Conclusion, you have created a policy with temporary permission using terraform functions such as timeadd and timestamp. This is good for scenarios where it is necessary to provision or use actions from a policy for a short time and then revoke access.
Successful!!
Top comments (0)