Update: A Better Way!
I found a much better, easier way to do this with Ansible. Read about that in this article. I think my enthusiasm for wanting to experiment with User Data made things a little tougher here, but I learned a few things.
“ubuntu” has no password
I wanted to bootstrap a server on an AWS EC2 instance in a fairly secure way, giving passwordless sudo to a user so that I could install software with Ansible.
The Ubuntu 22.04 distro provided by Amazon has a user “ubuntu” that can sudo su
without a password. The catch is that to use become: yes
in an Ansible playbook seems to require a password. By setting up passwordless sudo on the instance, I can use become and install my software as a sudo user.
Give sudo access
Typically, I run a bootstrapping role to add a new user that can sudo, but I wanted to see if I could do a little more automating.
I can see three ways to do this:
- Create the server how I want it, then turn it into an AWS AMI (Amazon Machine Image). I don’t think this has as much flexibility as running Ansible and being able to change a config on the fly.
- Use the “User Data” setting to run some commands while the server is being provisioned by Amazon.
- Attempt to streamline my typical bootstrap + software installation to make it a little less clunky.
Vendor Lock-In
With cloud configuration, part of the decision process is how much vendor lock-in there will be. In the choices above, the lock-in to AWS decreases with each option. Put another way, it’s easier to port our solution to another provider the lower we are in the list.
For most businesses, I would figure vendor lock-in isn’t a big deal, since the development costs of being cloud-agnostic can be pretty high.
Creating my server and saving an AMI from it would be the fastest and have the lowest development cost - I can run commands till I get what I want. Creating an Ansible playbook requires more development time but can be reconfigured more easily.
User Data: The Middle Road
I went with the middle road and used “User Data” in my playbook.
The User Data settings permits you to run a shell script on the launch of an EC2 instance.
Three things to remember about User Data:
- It should start with
#!/bin/bash
or the path to your chosen interpreter. - The script runs under the root user.
- The script only runs once - on boot during launch. (Although you can configure it to run on every restart.)
These characteristics make it pretty easy to do root-y activities without creating extra access to an instance.
Passwordless sudo
Normally, to give passwordless sudo to a user, you would edit the /etc/sudoers file with sudo visudo
and add this line to it:
someusername ALL=(ALL) NOPASSWD:ALL
After saving, you’d do sudo visudo -c
to make sure the file is correct and can be parsed.
Passwordless sudo with Ansible
We want to do the above, but automate it.
In an Ansible playbook, my User Data looks like this:
- name: Create EC2 Block
block:
- name: Create EC2 instances
amazon.aws.ec2_instance:
instance_type: "{{ instance_type }}"
user_data: |
#!/bin/bash
sudo apt update && sudo apt upgrade
sudo awk '/root\s+ALL=\(ALL:ALL\) ALL/ {print; print "ubuntu ALL=(ALL) NOPASSWD:ALL"; next}1' /etc/sudoers | sudo tee /etc/sudoers
image_id: "{{ image_id }}"
....
As an aside, originally I used this command in my User Data to add the line I needed:
echo "ubuntu ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers
However, I didn’t like how it only appended the line to the end of the file. This command:
sudo awk '/root\s+ALL=\(ALL:ALL\) ALL/ {print; print "ubuntu ALL=(ALL) NOPASSWD:ALL"; next}1' /etc/sudoers | sudo tee /etc/sudoers
will find the line for the root user and append after that line.
Shouldn’t I Install Other Software Here?
At this point, it seems like I should just add the other software I want to install here and forget the /etc/sudoers file!
This seems to make a lot of sense for throwaway or single purpose servers and cuts down on the amount of YAML I need to write for Ansible.
A good way to do this would be to create a base jinja2 template and create per host/group User Data variables to install software.
Security
Users should have the least permissions necessary to do their job. They shouldn’t have root access if they don’t need it.
For this instance, a personal server with low resources, no access to other resources, and a generally low threat level (I’m not running a bank here), I’m not too worried about intrusions.
Additionally, in this particular situation, an attacker would need to have gained access to my personal laptop to grab the SSH private key, which, in the scheme of things, is pretty unlikely.
I have some thoughts on increasing security:
- Enable, then disable passwordless sudo (using Ansible’s
lineinfile
to modify the /etc/sudoers file). This seems a little clunky. - Use a password. This is a lot less convenient, but I have read a password into Ansible using the Linux
expect
command. I’ve used this for personal projects and used a plaintext password. This requires access to my laptop, and attacker with this access would also get my SSH private key. - Use Ansible Vault. A great option, and probably what I would set up.