A step-by-step AWS lab with lots screenshots: create and delete an IAM user, host a site on S3, launch an EC2 server and run Jenkins on it, including the two errors I hit and how I fixed them.
Almost every AWS learning path starts with the same three moves: decide who is allowed to do what, store a file somewhere, and start a server. That is IAM, S3 and EC2, and nearly everything else on AWS sits on top of those three.
So I did all three for real, took a screenshot of every click, hit two errors along the way, fixed them, and then deleted everything at the end of each part so nothing was left running on my bill.
This post is the full walkthrough. By the end you will have:
- Created an IAM user, signed in as that user, forced a password reset, and deleted the user
- Created an S3 bucket, uploaded a website into it, made it public, and opened it in a browser
- Launched an Ubuntu server on EC2, connected over SSH, installed Java and Jenkins, and unlocked Jenkins in the browser
- Seen exactly what two common AWS errors look like, why they happen, and how to fix them
- Cleaned up every resource so you do not pay for what you are not using
The plan
| Part | Service | What I built | How I cleaned up |
|---|---|---|---|
| 1 | IAM | A user called 4thman with console access and a forced password reset |
Deleted the user |
| 2 | S3 | A bucket called 4thman-bucket serving my CV website |
Emptied and deleted the bucket |
| 3 | EC2 | An Ubuntu server called 4thman-server running Jenkins |
Stopped, then terminated the instance |
Before you start
- An AWS account. I worked as the root user for this lab (more on why that is not ideal in a moment).
- A terminal with SSH. I used Git Bash on Windows. macOS and Linux terminals work the same way.
- A small static website if you want to repeat the S3 part. I used my own CV website, a one-page site built from a Tooplate template called StackCraft: one
index.html, one CSS file, one JavaScript file and animagesfolder. Any simple HTML, CSS and JS site will do. - A second browser window in incognito or private mode, for testing the IAM user.
A note on cost: everything here is small, but "small" only stays small if you clean up. That is why every part ends with a teardown. Free tier and credits differ between accounts, so check your own billing page.
Part 1: IAM, deciding who gets to do what
Why I did not just keep using root
Look at the top right corner of the next screenshot. It says root. The root user owns the account and can do absolutely anything, including closing it. You use it to get started, and then you stop using it. Day-to-day work belongs to an IAM user with only the permissions it needs. That principle is called least privilege, and IAM is the service that enforces it.
AWS Console Home, signed in as the root user
Step 1: Open IAM
In the search bar at the top of the console, type iam and pick IAM from the Services list.
Searching for IAM in the console search bar
Step 2: Create the user
In the sidebar, open IAM users. The count says (0), so this account has no IAM users yet. Click Create user.
IAM users page with no users yet and the Create user button
Step 3: Fill in the user details
This screen has four decisions on it:
-
User name:
4thman - Provide user access to the AWS Management Console: ticked, because I want this user to be able to sign in on the website and not just use the CLI
- Console password: I chose Custom password so I could pick my own. The rules are at least 8 characters and a mix of at least three of: uppercase, lowercase, numbers and symbols.
- Users must create a new password at next sign-in: ticked. AWS marks this as recommended, and it is a good habit. Whoever receives the temporary password has to replace it with their own on first login.
User details: name, console access, custom password and forced reset
Step 4: Permissions
The next step offers three ways to give permissions: add the user to a group, copy permissions from another user, or attach policies directly. AWS recommends groups, because you manage "the Developers" once instead of managing every developer separately.
For this lab I did not want this user to be able to do anything yet, so I left the defaults alone and clicked Next.
Set permissions step, left on the defaults
Step 5: Review and create
The review page shows the choices I made: custom password, password reset required. The part I want you to notice is Permissions summary. I never attached a policy, but one is listed: IAMUserChangePassword. AWS adds it automatically when you force a password reset, because the user needs permission to change their own password. It lets them do that and nothing else, which is exactly least privilege at work.
Click Create user.
Review page showing IAMUserChangePassword as the only policy
Step 6: Save the sign-in details
The next screen is the only time AWS shows you the console password. It also gives you the sign-in URL, which follows this pattern:
https://<your-account-id>.signin.aws.amazon.com/console
Copy the details or click Download .csv file before leaving this page.
User created, with the console sign-in URL, user name and password
Step 7: Sign in as the new user
I opened an incognito window so I could stay signed in as root in my normal window. On the IAM user sign-in page I entered the account ID, the user name 4thman and the temporary password.
Signing in as the new IAM user in an incognito window
Straight after clicking Sign in, AWS showed this:
Password reset screen shown at first sign-in
That message says the password "has expired or requires a reset", which sounds like a problem. It is not. It is the "must create a new password" option from Step 3 doing its job. I entered the old password once, the new one twice, watched the green Matches appear, and clicked Confirm Password Change.
Password reset successful
Then I signed in again with the new password and landed on the console as 4thman. Notice the account menu at the top right now shows the user name instead of root.
Console Home as the 4thman IAM user
One thing to watch: this console opened in Europe (Stockholm), because the sign-in page had redirected me to a Stockholm sign-in endpoint. IAM users are global, so that does not affect the user. But most other services are regional, so always check the region selector before you create anything.
This user can open the console but cannot do much with it. The only policy attached lets them change their own password. That is the point of starting with nothing and adding permissions deliberately.
Step 8: Delete the user
Back in my root window, the IAM users list now shows 4thman with real activity: last activity and console sign-in a couple of minutes ago, password age of a few minutes. I ticked the box next to the user and clicked Delete user.
Selecting the user and clicking Delete user
AWS makes you type confirm before it deletes anything. It is a small speed bump on purpose. Deleting a user also removes its credentials, so there is no undo.
Typing confirm to delete the user
IAM done. One user created, tested and removed.
Part 2: S3, storing files and serving a website
What S3 actually is
S3 is object storage. You create a bucket (a container), and everything you put in it is an object. Two details are worth knowing before you start:
- Bucket names are unique across all AWS accounts in the world, not just yours.
- S3 has no real folders. When the console shows an
images/folder, that is just a shared prefix in the file names. You will see this when the folder appears in the object list later.
Step 1: Find S3 and create a bucket
Search for s3 buckets and open S3. If you have no buckets yet, the landing page shows a big Create bucket button.
Searching for S3 in the console
The S3 landing page and its Create bucket button
Step 2: Name the bucket
I confirmed the region was US East (N. Virginia) us-east-1, kept the type as General purpose, kept the default global namespace, and named the bucket 4thman-bucket. Bucket names must be 3 to 63 characters and can only contain lowercase letters, numbers, periods and hyphens.
Region, bucket type and the bucket name 4thman-bucket
Step 3: Leave the safe defaults alone
Scroll down and you will see two settings I deliberately did not change:
- Object Ownership: ACLs disabled (recommended). Access is controlled through policies only.
- Block all public access: ticked. All four sub-settings are on.
AWS builds buckets private by default, and that is a good thing. These two defaults are also the reason I hit an error in a few minutes, so keep them in mind.
Object Ownership with ACLs disabled and Block all public access ticked
At the bottom, default encryption is already on (SSE-S3, with a bucket key enabled). I left it and clicked Create bucket.
Default encryption settings and the final Create bucket button
Step 4: Upload the CV website
The bucket now exists and says Objects (0). Click Upload.
Bucket created and empty, with the Upload button
I added the four files that make up my CV website: index.html, tooplate-stackcraft-script.js, tooplate-stackcraft-style.css, and about.jpg inside an images/ folder. The upload summary reports 4 files totalling 229.1 KB with 0 failures. Notice the destination, s3://4thman-bucket, and that about.jpg shows images/ in the Folder column.
Upload succeeded: 4 files, 229.1 KB, 0 failed
The files are in, but because the bucket is private, nobody on the internet can open them yet. Time to change that.
Error 1: "Access denied" when making the files public
I went back to the object list, ticked every file and folder, and opened the Actions menu to choose Make public using ACL.
)
Selecting all objects and opening the Actions menu
The next page shows two banners, and both matter:
- A green one: Successfully enabled ACLs (Bucket owner preferred). Because I created the bucket with ACLs disabled, and "Make public" works by setting an object ACL, the console switched the ownership setting for me.
- A blue one: Public access is blocked because Block Public Access settings are turned on for this bucket.
I read the green one, ignored the blue one, and clicked Make public.
First Make public attempt, with the ACL banner and the Block Public Access warning
The result, for all four objects:
Failed to edit public access: Access denied on all four objects
Failed to edit public access: Access denied.
Reading the error properly
My first instinct with "Access denied" was to check my permissions. But I was the root user, so permissions were not the problem. The blue banner had already told me the cause.
There are two separate gates on public access in S3:
- Object ACLs and ownership, which decide how an individual object can be made public. The console fixed this one for me.
- Block Public Access, a bucket-level safety switch that overrides ACLs and bucket policies. It was still on, and it wins every time.
Even if the object says "public", Block Public Access says "no".
The fix
Open the bucket and go to the Permissions tab.
Opening the Permissions tab of the bucket
Under Block public access (bucket settings) it says Block all public access: On. Click Edit.
)
Block all public access is On, with the Edit button
Untick Block all public access. That clears all four boxes underneath it. Click Save changes and confirm the warning S3 shows you.
)
Block all public access unticked, then Save changes
Now repeat the original action. Back on the Objects tab I selected everything again and opened Actions.
Selecting all objects again and opening Actions
On the Make public page, I clicked Make public again. This time there was no blue warning.
Second Make public attempt, with no blocking warning
The summary now reads: Successfully edited public access, 4 objects, 229.1 KB, 0 failed.
Successfully edited public access: 4 objects, 0 failed
See it live
I ticked index.html only and clicked Copy URL. The console confirmed with Object URL Copied.
Copying the object URL of index.html
Pasting it into a new tab loaded my full CV website straight from S3, with my name, role, skills, work and experience sections all in place. The URL has this shape:
https://<bucket-name>.s3.<region>.amazonaws.com/index.html
The CSS, JavaScript and image all loaded as well, because I made every object public, not just the HTML file.
My CV website loading straight from S3
A word of caution. Opening a whole bucket to the internet is fine for a throwaway demo. For a real site, keep the bucket private and put CloudFront in front of it, or at least use a bucket policy that only allows reading the specific objects you want public.
Cleanup: empty the bucket, then delete it
S3 will not delete a bucket that still has objects in it. I selected everything and clicked Delete.
Selecting all objects and clicking Delete
S3 asks you to type permanently delete to confirm. This cannot be undone. (The Empty button on the bucket list does the same job in one step.)
Typing permanently delete to confirm
With the bucket empty, I went back to the bucket list, selected 4thman-bucket and clicked Delete.
Selecting the empty bucket and clicking Delete
To confirm, I typed the bucket name. Read the warnings on this page: once a bucket is deleted, its name is released and another AWS user can claim it.
Typing the bucket name to confirm deletion
S3 done. A bucket created, a website served from it, one error fixed, everything removed.
Part 3: EC2, a server in the cloud, running Jenkins
The vocabulary you need first
| Term | What it means |
|---|---|
| EC2 instance | A virtual server running in AWS |
| AMI | The image the server starts from: operating system plus preinstalled software. I used an Ubuntu image |
| Instance type | The size of the machine. I used t3.micro (2 vCPU, 1 GiB memory) |
| Key pair | A public and private key used to log in over SSH. AWS keeps the public half. You keep the private .pem file |
| Security group | A virtual firewall around the instance that decides which ports accept traffic |
| Jenkins | An open source automation server, widely used to build, test and deploy code (CI/CD) |
Step 1: Launch the wizard
Search for ec2 and open EC2. (The green banner partly visible behind the search box is the bucket deletion confirming.)
Searching for EC2 in the console
On the EC2 landing page, click Launch instance.
The EC2 landing page and its Launch instance button
AWS then offered a choice of walkthroughs. I picked Learn about EC2 and launch test instances. That is why a Tutorial panel appears on the right side of the next screenshots: it lists the steps and, handily, ends with a reminder to terminate your instance.
Choosing the Learn about EC2 walkthrough
Step 2: Name it and pick an image
I named the instance 4thman-server, chose Ubuntu from the Quick Start images, and left the number of instances at 1. The Summary panel on the right tracks everything you choose: image, instance type, firewall and storage.
Naming the instance, choosing Ubuntu and setting the number of instances
Step 3: Instance type and key pair
I picked t3.micro. Scrolling down to Key pair (login), I clicked Create new key pair.
Instance type and the Create new key pair link
I named it 4thman-key, kept RSA as the type and .pem as the format (the format for OpenSSH, which is what Git Bash uses), and clicked Create key pair.
Creating the key pair 4thman-key as RSA and .pem
The browser downloaded 4thman-key.pem immediately.
4thman-key.pem downloaded by the browser
Guard this file. AWS only keeps the public half of the key. This download is the only copy of the private half. Lose it and you cannot SSH into the server. Never commit it to GitHub.
Step 4: Network and security group
In Network settings:
- Auto-assign public IP: Enable, so the server is reachable from the internet
- Create security group (a new firewall for this instance)
-
Allow SSH traffic from: Anywhere (
0.0.0.0/0), so I can log in - Allow HTTP traffic from the internet: ticked
- Allow HTTPS traffic from the internet: left unticked
AWS shows a yellow warning that 0.0.0.0/0 means every IP address. For a short-lived lab that is acceptable. For anything longer, choose My IP instead of Anywhere.
Network settings: public IP, new security group, SSH and HTTP allowed
Step 5: Review and launch
Storage stayed at the default 8 GiB gp3 root volume. The Summary panel showed one instance, the Ubuntu image, t3.micro, a new security group and one 8 GiB volume. I clicked Launch instance.
Summary panel and the Launch instance button
A note on the screenshots in this section. Some of the launch screenshots (the key pair, security group, launch summary, success banner and running view) were captured on an earlier launch of the same setup, so the instance size, instance ID and IP address differ from the server used in the SSH and Jenkins steps. The clicks are identical. When I talk about the server's public IP from here on, use the one on your own instance page.
AWS replied with a green Success banner and the new instance ID. The page also suggests next steps, including Create billing usage alerts. Next time I will set that up before launching anything.
Instance launch initiated successfully
On the Instances page the new server went to Running, and the status checks passed. In the details panel, copy the Public IPv4 address. You need it to connect.
Instance running, with its public IPv4 address
Step 6: Lock down the key file and connect over SSH
While you are here, fix the key's permissions. SSH refuses to use a private key that other users on the machine can read, so restrict it:
cd Downloads
ls -l 4thman-key.pem
chmod 400 4thman-key.pem
ls -l 4thman-key.pem
chmod 400 means read-only for the file owner and nothing for anyone else. In the screenshot the listing goes from -rw-r--r-- to -r--r--r--. Because this is Git Bash on a Windows drive, permission bits do not map exactly like on Linux, but SSH accepted the key anyway.
Restricting the key file with chmod 400
Now connect. The default user on Ubuntu images is ubuntu:
ssh -i "4thman-key.pem" ubuntu@<your-public-ip>
The first time, SSH asks whether you trust this host. Type yes. The Ubuntu welcome banner appears and the prompt changes to ubuntu@ip-172-31-25-206:~$, which means every command from now on runs on the server in AWS and not on my laptop.
Connected to the server over SSH
Step 7: Update the server
sudo apt update
sudo apt upgrade -y
apt update refreshes the list of available packages. apt upgrade -y installs updates without asking for confirmation. On a fresh image there was nothing pending, and apt reported All packages are up to date.
sudo apt update and sudo apt upgrade -y
Step 8: Install Java
Jenkins is a Java application, so Java comes first. fontconfig is a small dependency Jenkins needs for rendering fonts.
sudo apt install fontconfig openjdk-21-jre -y
Installing fontconfig and OpenJDK 21
Check it worked:
java -version
You should see an OpenJDK 21 version string. You can see mine at the top of the screenshot in the next step.
Step 9: Add the Jenkins repository
Ubuntu's default repositories do not include Jenkins. The Jenkins project publishes its own package repository, so we add that and tell apt to trust its signing key:
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" \
| sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
What each piece does:
-
mkdir -p /etc/apt/keyringsmakes sure the folder for trusted keys exists -
wgetdownloads the Jenkins signing key into it, so apt can verify the packages really come from the Jenkins project -
echo ... | sudo teewrites a new repository entry that points atpkg.jenkins.ioand says which key to check it against -
apt updatereads the new repository. In the output you can seepkg.jenkins.io/debian-stablebeing fetched
The key file name contains a year. If this step fails, check the official Jenkins installation page for the current key name.
Java version check, Jenkins signing key, repository entry and apt update
Step 10: Install Jenkins
sudo apt install jenkins -y
The output shows Jenkins 2.580.1 (about 53.9 MB) installing along with a small helper package called net-tools. At the very end, Ubuntu prints a message about a pending kernel upgrade and suggests a reboot. That is a notice, not an error. Jenkins installs and runs fine without the reboot.
Installing Jenkins with apt
Step 11: Start Jenkins and check it
sudo systemctl enable --now jenkins
sudo systemctl status jenkins
enable --now does two things at once: it starts Jenkins immediately and makes it start automatically every time the server boots. status shows the result. Look for the green active (running) line, and notice the Java process and the memory use (about 300 MB for me). Press q to leave the status view.
Enabling Jenkins and confirming active (running)
Jenkins is running. Time to open it in the browser.
Error 2: "This site can't be reached"
I took the server's public IP, added Jenkins' default port, and opened http://<your-public-ip>:8080 in Chrome.
Chrome showing ERR_CONNECTION_TIMED_OUT on port 8080
This site can't be reached. The server took too long to respond. ERR_CONNECTION_TIMED_OUT.
Reading the error properly
I had just seen Jenkins report active (running), so the service was fine. The clue is in the wording:
- A connection refused error means the machine answered and said "nothing is listening on that port". That points at the service.
- A timed out error means the packets never got an answer at all. Something in between silently dropped them. On AWS, that is almost always the security group.
When I created the instance, I only allowed port 22 (SSH) and port 80 (HTTP). Jenkins listens on 8080, and nothing allowed that port in.
The fix: open port 8080 in the security group
In the EC2 console, open Instances, select the server, and click the Security tab. Under Security groups, click the group's link (mine was launch-wizard-2).
Instances, Security tab and the security group link
On the security group page, the Inbound rules tab lists only two rules, HTTP and SSH. Click Edit inbound rules.
Inbound rules showing only HTTP and SSH, with Edit inbound rules
Click Add rule. A new row appears with the type already set to Custom TCP and the port at 0.
Edit inbound rules with the Add rule button
Set the port range to 8080 and the source to Anywhere-IPv4 (0.0.0.0/0). AWS warns again that this opens the port to everyone, which is fine for a lab and something to tighten later. Click Save rules.
Custom TCP rule for port 8080 from Anywhere-IPv4, then Save rules
Security group changes apply instantly. No restart, no reboot. I reloaded the browser tab and the error was gone.
Unlock Jenkins
Jenkins greeted me with Unlock Jenkins. The browser shows Not secure in the address bar because we are on plain HTTP and not HTTPS. That is expected for a quick lab, and I will come back to it at the end.
The Unlock Jenkins page at port 8080
The page says the first admin password lives in a file on the server. Back in the SSH session:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
You need sudo because only root can read that file.
Reading the initial admin password on the server
I copied the long string it printed, pasted it into the Administrator password box and clicked Continue.
Pasting the administrator password and clicking Continue
Next came Customize Jenkins, with two choices: Install suggested plugins (the safe default) or Select plugins to install. Jenkins 2.580.1 is up and answering on my server's public IP.
Customize Jenkins: install suggested plugins or select plugins
That is where I stopped for this post. From here Jenkins would install its plugins and ask you to create your first admin user. The goal of this lab was getting Jenkins running on a server I built myself, and that is done.
Cleanup: stop and terminate the server
This is the part people skip, and it is the part that costs money. In the EC2 dashboard, the Resources box shows exactly what is still alive: 1 running instance, 1 key pair, 3 security groups and 1 volume.
EC2 dashboard Resources box before cleanup
I selected the instance and opened the Instance state menu.
Selecting the instance and opening the Instance state menu
I stopped it first. The status moves to Stopping.
Instance stopping
Then I terminated it. Terminated means deleted for good, so the state ends at Terminated, as in the final screenshot. Both green banners are visible here: stopping and termination were both initiated.
Instance terminated
A difference worth remembering: stopped is not deleted. A stopped instance stops charging for compute, but its storage volume still exists and still bills. Terminate when you are done.
The key pair and security groups stay behind after termination. They are free, but delete them in the EC2 console if you will not reuse them, and delete the .pem from your computer too.
The two errors, side by side
| Where | What I saw | Real cause | Fix | |
|---|---|---|---|---|
| 1 | S3, Make public | Failed to edit public access: Access denied on all 4 objects | Block Public Access was on, and it overrides ACLs. (ACLs were also disabled by default, which the console fixed for me.) | Permissions tab, Edit Block public access, untick, Save. Then Make public again |
| 2 | Jenkins in the browser | ERR_CONNECTION_TIMED_OUT on port 8080 | The security group only allowed ports 22 and 80 | Add an inbound Custom TCP rule for 8080 |
And three smaller things that look scarier than they are:
- "Password has expired or requires a reset" at first IAM sign-in is the forced reset working as designed
- "Pending kernel upgrade" after installing Jenkins is a reboot suggestion, not a failure
-
chmod 400showing-r--r--r--on Git Bash is a Windows quirk, and SSH was fine with it
What to would do differently during production
This lab was about learning the moves, so I took shortcuts that I would not take on anything real:
- Stop using root. I worked as root all the way through, which is the exact habit Part 1 is meant to break.
-
Do not use
0.0.0.0/0for SSH. Pick My IP so only my own address can reach port 22. - Restrict port 8080 too, and put Jenkins behind HTTPS with a reverse proxy before anyone enters a real password into it.
- Keep S3 buckets private and serve public sites through CloudFront.
- Set a billing alert first, before launching anything.
- Use groups for permissions, not individual users.
Wrap up
Three services, three cleanups, two errors:
- IAM taught me that new users start with almost no power, and that is a feature
- S3 taught me that "Access denied" does not always mean your permissions, and that public access has more than one gate
- EC2 taught me that a timeout and a refusal are different problems, and that the security group is the first place to look when a running service will not answer
Every one of the fixes took less than a minute once I understood the message. The hard part was reading the error properly, not fixing it.
Tried it yourself and ran into a different error? Drop the exact error message in the comments, and let’s troubleshoot it together! If this lab helped you, show some love, hit follow, and join me as I tackle new challenges and share real lessons from my cloud and DevOps journey, one lab at a time.



































































Top comments (0)