Generating temporary access actions in IAM policies using Terraform with DATE and TIME conditions in AWS.
Tags: aws, iam, terraform, tutorial
Published: 2024-01-03 22:02:14


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
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
* [Terraform](https://www.terraform.io/downloads.html)

Let's see how we can do this using terraform.

## Reference Architecture

![SysOps1 architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kszcngd7tas2u22tew5r.png)

This is the link for all code terraform in branch sysops1.

{% embed https://github.com/segoja7/SysOps.git %} 

## Step 1.
Adding the [kms-module](https://registry.terraform.io/modules/terraform-aws-modules/kms/aws/latest?tab=outputs) 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](https://registry.terraform.io/modules/terraform-aws-modules/ec2-instance/aws/latest) 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](https://developer.hashicorp.com/terraform/language/functions/timestamp) and [timeadd](https://developer.hashicorp.com/terraform/language/functions/timeadd).
And this link for AWS [conditions policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws-dates.html)

## 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
```


![Terraform apply](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k4j0i5zvyn83uhco0kql.png)

## Step 7.
Check the resources that were created.

An Ec2 bastion with an instance profile.

![Ec2 bastion with an instance profile](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rjlwodx9lts4wb14ciy1.png)

Using cloud-init.yaml this is a part of the userdata result.

![userdata ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7yrec93tzu8dqaqo1mud.png)

A Role with permission policies.

![Role with permission policies](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvayfjunhk8v718yhpkq.png)


![policy with date and time](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/29s08660zwvqmn4gd603.png)
> 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.

![A secret with password ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/29siix5e0pds0ubim8gk.png)

## 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
```

![aws sts get-caller-identity](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5yx7rb7iq8dsuthq8mg.png)


try retrieve the secret.

```
aws secretsmanager get-secret-value --secret-id secrets/bastion/segoja7 --query SecretString --region us-east-1
```

![aws secretsmanager get-secret-value](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6wa9j85mpwboo94cck8.png)



## Step 9.

connecting with user created in execution time using userdata.

![retrieve secret](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b48d6jiqpujy4ln2riif.png)


![using secret](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gcyff90k62rv1abken77.png)

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!!