A better way of setting up and managing user ssh keys in AWS EC2 instances Oct 29 2022

I've been using AWS EC2 instances for a while now, and I've always struggled to find a clean way to manage the users and ssh keys for the instances. I've tried a few different approaches and settled on one that I think is the best so far.

In this article, I'll create a regular EC2 instance as an example. But you can use the same approach to set up users and ssh keys or run other commands on any other type of EC2 instance (e.g. Bastion hosts, EKS nodes, ECS nodes, etcetera). The key is using cloud-init.

Prerequisites

What is could-init?

Quote from the official documentation:

"Cloud-init is the industry standard multi-distribution method for cross-platform cloud instance initialization."

What it means for us is that we can use it to configure our instances in AWS. We can use it to create users, install packages, etc. And that is exactly what we are going to do. We will use a file in the cloud-init format containing our users and their respective ssh keys.

Creating the EC2 instance

First, we need to create the EC2 instance. We can do that by creating a main.tf file with the following content:

1
2
3
4
5
6
7
8
9
10
    provider "aws" {
      region = "us-east-1"
    }

    resource "aws_instance" "example" {
      ami           = "ami-XXXXX"
      instance_type = "t2.micro"

      user_data = file("cloud-init.yaml")
    }

I created a simple EC2 instance with the t2.micro instance type and the ami-XXXX AMI. The user_data attribute is where we are going to specify the cloud-init file that we are going to create. The instance type and the AMI are random; ignore their values.

If you want to find Amazon Linux 2 AMI IDs, you can use the following command:

1
aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn*" --query 'sort_by(Images, &CreationDate)[].Name'

reference: Query for the latest Amazon Linux AMI ids

Or, if you want to query Ubuntu AMI IDs, you can use the following command:

1
aws ec2 describe-images --owners 099720109477 --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*" --query 'sort_by(Images, &CreationDate)[].Name'

Ok, after that slight detour, let's get back to setting up the users and their ssh keys.

Creating the cloud-init file

We now need to create the cloud-init.yaml file that will contain our users:

1
2
3
4
5
6
7
8
9
10
11
12
13
#cloud-config
users:
  - name: rderik
    ssh_import_id:
      - gh:rderik
    lock_passwd: true
  - name: pascual
    ssh_import_id:
      - lp:pascual_fake
  - name: adrian
    ssh_import_id:
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSL7uWGj8cgWyIOaspgKdVy0cKJ+UTjfv7jBOjG2HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX fake-public-key

As you can see, we are creating three users: rderik, pascual and adrian. The first two users use the ssh_import_id attribute to import their ssh keys from GitHub and Launchpad, respectively. The third user uses the ssh_authorized_keys attribute to specify the ssh keys directly.

And that is it. We are now ready to run terraform apply and create the EC2 instance.

Connecting to the EC2 instance

Now that we have created the EC2 instance, we can connect to it using the ssh keys specified in the cloud-init.yaml file. We can do that by running the following command:

1
ssh -i ~/.ssh/id_rsa rderik@<ec2-instance-ip>

If you use a different ssh key, you can use that instead of ~/.ssh/id_rsa.

And that is it. We have created an EC2 instance with three users and their respective ssh keys. We can now use the ssh_import_id, and ssh_authorized_keys attributes to create as many users as we want and specify their ssh keys.

Final thoughts

I hope you found this article useful. I believe it is better to have the users and their ssh_keys defined in our IaC code instead of manually creating them. This way, we can easily create new users and their ssh keys by adding them to the cloud-init.yaml file.

could-init is a very powerful tool that can be used for much more than just creating users and ssh keys. I encourage you to check the official documentation. Check the Module Reference. You can see what other options you can pass to the Users and Groups and many more. I bet that you'll find something useful that you can add to your instances.

One last thing, if you don't like having a separate file for the could-init configuration, you can specify it directly in your terraform template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    provider "aws" {
      region = "us-east-1"
    }

    resource "aws_instance" "example" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t2.micro"

      user_data = "#cloud-config\n${yamlencode({
        users           = [
          { name          = "rderik"
            ssh_import_id = "gh:rderik"
            ssh_authorized_keys = []
          },
          { name          = "pascual"
            ssh_import_id = "lp:pascual_fake"
            ssh_authorized_keys = []
          },
          { name          = "adrian"
            ssh_import_id = ""
            ssh_authorized_keys = [
              "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSL7uWGj8cgWyIOaspgKdVy0cKJ+UTjfv7jBOjG2HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX fake-public-key",
            ]
          },
        ]
      })}"
    }

Notice how we use the yamlencode function to convert the cloud-init configuration to a string. This way, we don't need to create a separate file for the cloud-init configuration. And also, don't miss the first line, #cloud-config.

References


** If you want to check what else I'm currently doing, be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message, you can send it to derik@rderik.com.