All posts Reading time: 6 minutes

Customizing the default user of an Linux AMI on AWS

AWS Linux


I like creating custom AMIs to ease server configuration of my AWS environments, my personal approach is to create a base AMI with some basic tools that will be used in my EC2 instances and then use it as starting point to create other AMIs. Over the years I found that it has a lot of benefits but can also demand many hours of management work if you don’t set up things properly. One of the first things I like to do is customizing the default user of my AMIs, not just for consistency but also to sleep better at night. Cause I don’t know about you, but having a user defined by AWS that everybody knows its username makes me scary.

Background


In the past, I was doing it by creating a new user and deleting the default one but that was just generating more maintenance work. In short, the biggest problem relates to credentials management. It is simply too hard to have a custom AMI that will work for the majority of situations when you have a user with a pre-defined SSH key associated with it. For me, the solution would be having an AMI with a custom user but leveraging the ability to specify a Key Pair during launch time as you can do for the official AMIs.

I found the solution for my problem after seeing this article by Eric Hammond. In his article, he teaches two ways of customizing the default user during launch time using EC2’s user data, but what he presents is not enough to make a change that will persist after shutting down the instance and creating an AMI from it.

The solution


The final solution is easy, it simply consists of configuring a Cloud-Init definition file and placing it inside of /etc/cloud/cloud.cfg.d/ so it will be run by Cloud-Init during each boot time. By doing that, the EC2 service can be aware of the custom user after the AMI creation and configure the SSH-authorized keys accordingly.

BTW I’ve only tested this solution with the official Ubuntu images (Xenial and Trusty), but it should work with other Linux distros as well as long as they have Cloud-Init installed which seems to be the standard for all official AMIs.

To make the AMI I will be using Hashicorp’s Packer which is an awesome tool for creating machine (and container) images in a repeatable way, but the same configuration can be done manually (sad face).

Using Packer


There are two important files in the repo, the first one is called defaults.cfg which is a Cloud-Init configuration file, that will be used as input for the EC2 user data and also placed in the /etc/cloud/cloud.cfg.d/ folder so the configuration can persist after we create an AMI from it, here is how it looks:

#cloud-config
system_info:
  default_user:
    name: jklimber


This file basically informs the system that the default user is called jklimber (of course you can change that to whatever makes sense to you).

The second file is called packer.json, it is the Packer template file which defines how the AMI will be created. The template is really easy to understand even if you are not familiar with Packer, to follow up the lines you simply need to know how to create AMIs manually. Let’s take a look at it before going into details:

{
  "builders": [
    {
      "name": "ubuntu-xenial-base-ami",
      "type": "amazon-ebs",
      "region": "us-east-1",
      "source_ami": "ami-cd0f5cb6",
      "instance_type": "t2.micro",
      "ssh_username": "jklimber",
      "ami_name": "{{ build_name}}-{{isotime \"2006-01-02\"}}",
      "tags": {
        "Name": "Ubuntu Xenial base",
        "OS_Version": "Ubuntu",
        "Release": "Xenial"
      },
      "ami_block_device_mappings": [
        {
          "device_name": "/dev/sda1",
          "volume_size": 30,
          "volume_type": "gp2",
          "delete_on_termination": true,
          "encrypted": false
        }
      ],
      "encrypt_boot": "false",
      "ami_description": "Ubuntu 16.04 LTS base image",
      "force_deregister": true,
      "user_data_file": "./defaults.cfg"
    }
  ],

  "provisioners": [
    {
      "type": "file",
      "source": "./defaults.cfg",
      "destination": "/tmp/defaults.cfg"
    },
    {
      "type": "shell",
      "inline": [
        "sudo mv /tmp/defaults.cfg /etc/cloud/cloud.cfg.d/defaults.cfg"
      ]
    }
  ]
}


In general lines, this what packer will do:

  • It will launch a new t2.micro instance using the official Ubuntu Xenial AMI ("instance_type": "t2.micro" and source_ami": "ami-cd0f5cb6")
  • It will use an user data file during the instance launch ("user_data_file": "./defaults.cfg")
  • Since the user data file is setting a new default user, it will connect to the instance using the jklimber user ("ssh_username": "jklimber")
  • Copy the defaults.cfg file to a temp folder ({ "type": "file", "source": "./defaults.cfg", "destination": "/tmp/defaults.cfg" })
  • Run a command to copy the defaults.cfg file to its definitive folder ("sudo mv /tmp/defaults.cfg /etc/cloud/cloud.cfg.d/defaults.cfg")

To create the AMI you simply need to run packer build packer.json form your terminal and wait for the results, be aware that you need to have some AWS credentials pre-configured in your machine, it can be done by installing the AWS CLI, running aws configure, and following the additional steps of the wizard. After the completion you should see something like this:
Packer build preview
Packer build preview

newly-created AMI, don’t forget to select a Key Pair of your preference to connect to the instance:

Selecting a key pair
Selecting a key pair

After the instance launch, it is possible to validate the results by connecting to it with the username you specified:
Login as the configured user
Login as the configured user

Also, you can confirm that the former default user, in this case, ubuntu wasn’t even created:
List existing users
List existing users