Inspect and Harden a Linux Container
Inspect a Rocky Linux 9 container and harden it with least-privilege permissions.
Introduction
30 Second Summary
When a container image lands on your desk for production deployment, the first question isn't "does it work?" It's "is it safe?" Every Linux Cloud Engineer needs a repeatable process for inspecting an unknown system and locking it down before it touches a cluster.
In this project, you will execute a production onboarding runbook against a live Rocky Linux 9 container. You'll identify the system, establish a package baseline, create a non-root service account, and harden file permissions using the same checklist applied before container images are approved for Kubernetes.
What You'll Build
Picture yourself shelling into a fresh container, running your security checklist, and confidently signing off that it's production-ready because you've verified every permission and locked out every unnecessary access path.
By the end of this project, you'll have:
- A live Rocky Linux 9 container you can shell into and inspect using the same commands you'd run on a real server.
- A locked-down application directory with a dedicated non-root service user that can operate the app without dangerous world-writable permissions.
- Firsthand proof of why chmod 777 fails security audits, and the correct least-privilege fix that Kubernetes Pod Security Standards require.
- Secret Mission: Write a hardening validation script that audits a container for world-writable files and SUID binaries.
Are there any prerequisites?
This project assumes you have Docker Desktop installed on Windows and basic experience pulling images and running containers. If you've completed "Containerize a RAG API with Docker" or similar hands-on Docker work, you're ready to go.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Launch Your Rocky Linux Container
Before you can inspect or harden a system, you need a running system to work on. Launching a Rocky Linux 9 container gives you an isolated Linux server to practice on without risking anything on your host machine.
Rocky Linux is a RHEL-compatible distribution used in enterprise container workloads. You'll launch it as a container using Docker, giving you a safe environment you can inspect and harden freely.
In this step, get ready to:
- Pull and launch a Rocky Linux 9 container as your practice server.
- Confirm the container is running and note its status.
Pull and launch the Rocky Linux 9 container
The docker run command handles both pulling the image from Docker Hub and starting the container in one step. You'll launch it in detached interactive mode so it stays running in the background while you work.
Why Rocky Linux?
Enterprise container workloads on AKS and other Kubernetes clusters frequently use RHEL-family base images. Rocky Linux 9 is the community rebuild of RHEL 9, making it the best free practice ground for the RHEL skills you'll need in production.
- In your PowerShell window, launch the Rocky Linux 9 container by running this command:
docker run -dit --name rocky-server rockylinux/rockylinux:9 /bin/bash
What does this command do?
- docker run pulls the image (if not already present) and creates a new container from it.
- -dit combines three flags: detached (-d) runs it in the background, interactive (-i) keeps STDIN open, and tty (-t) allocates a pseudo-terminal.
- --name rocky-server gives the container a human-readable name so you can reference it easily.
- rockylinux/rockylinux:9 is the rolling major-version tag for the Rocky Linux 9 image on Docker Hub.
- /bin/bash tells the container to run bash as its primary process, keeping it alive so you can exec into it later.
- You should see a long hexadecimal string printed to the terminal. This is the full container ID, confirming the container launched successfully.
Seeing an error about the container name?
If you see a message about the name rocky-server already being in use, a previous attempt left a container behind. Remove it by running docker rm -f rocky-server and then re-run the docker run command above.
Help me troubleshoot my docker run command
Confirm the container is running
Before you move on, take a moment to predict what you expect to see. If the container launched correctly, what status should it show?
- Check the running containers by running this command:
docker ps
You should see rocky-server listed with a status of Up.
What do these columns mean?
- CONTAINER ID is the shortened version of that long hex string you saw earlier.
- IMAGE shows rockylinux/rockylinux:9, the base image your container was built from.
- STATUS shows how long the container has been running. Anything starting with "Up" means it is alive and healthy.
- NAMES shows rocky-server, the friendly name you assigned with --name.
Don't see rocky-server in the list?
If docker ps shows nothing, the container may have exited immediately after starting. Run docker ps -a to see all containers including stopped ones.
If the STATUS shows "Exited", remove it with docker rm rocky-server and re-run the docker run command from the previous substep. Make sure Docker Desktop is running and not paused.
Help me figure out why my container exited
Your Rocky Linux 9 container is alive and waiting. Next up, you'll exec into it and start identifying exactly what system you're dealing with.
Identify the System
Your Rocky Linux container is up and running. Now it's time to get inside it and answer the first question on any maintenance runbook: what exactly is this system?
On AKS, containers can run a variety of base images. Before making any changes, you need to confirm the OS version, check what's running, and understand the filesystem layout. This gives you the baseline to work from.
In this step, get ready to:
- Confirm the OS identity of your container.
- Review filesystem disk usage and running processes.
- Examine the root directory structure and permissions.
Exec into the container and confirm the OS
To run commands inside the container, you need an interactive shell session. Think of this as SSH-ing into a remote server, except the server is a Docker container on your local machine.
- In your PowerShell window, exec into the running container by running this command:
docker exec -it rocky-server /bin/bash
What does this command do?
- docker exec runs a command inside an already-running container.
- -it combines two flags: -i keeps STDIN open so you can type, and -t allocates a pseudo-TTY so the shell displays properly.
- rocky-server is the container name you set in Step 1.
- /bin/bash starts a Bash shell session inside the container.
Your prompt changes to something like [root@<container-id> /]#. You are now inside the Rocky Linux container as the root user.
Prompt didn't change?
- Make sure the container is still running. Run docker ps in a new PowerShell tab to check its status.
- Check for typos in the container name. It must be exactly rocky-server.
- If the container exited, restart it with docker start rocky-server and try the exec command again.
Help me troubleshoot my docker exec command
Before you run the next command, what OS do you expect to see reported?
- Confirm the OS identity by running:
cat /etc/os-release
You should see output that includes NAME="Rocky Linux" and PLATFORM_ID="platform:el9". The el9 confirms this is a RHEL 9 compatible system.
Why does platform:el9 matter?
The el9 platform ID tells you this system uses the same package ecosystem, library versions, and kernel features as RHEL 9. Any dnf packages, configuration guides, or security baselines written for RHEL 9 apply directly to this container.
Check filesystem disk usage and running processes
Now that you know the OS identity, check what resources are in use and what processes are active.
- Check how the filesystem's disk space is allocated by running:
df -h
The -h flag shows sizes in human-readable format (GB, MB). You will see the overlay filesystem that Docker uses to layer the container's files on top of the host's storage.
- List all running processes by running:
ps aux
Notice how minimal the process list is. You should see only a few entries, likely just your Bash shell sessions.
Why so few processes?
A full Linux server runs dozens of background services (SSH, cron, logging, networking). A container runs only the processes you explicitly start. This is the container's isolated process tree.
Fewer processes means a smaller attack surface. This is one reason containers are favored for production workloads on Kubernetes.
Examine the root filesystem layout
The final piece of system identification is understanding what directories exist and who owns them.
- Examine the root filesystem layout by running:
ls -la /
You should see the standard RHEL directory structure: /bin, /etc, /home, /opt, /usr, /var, and others.
What do the permission strings mean?
Each line starts with a permission string like drwxr-xr-x. The first character indicates the type (d for directory, - for file). The next nine characters show read, write, and execute permissions for owner, group, and others.
Notice that almost every directory is owned by root. This is the default state. In the next steps, you will create a non-root service user and manage these permissions for production safety.
Seeing 'command not found' for any command?
- Confirm you are inside the container (your prompt should show [root@... /]#, not the PowerShell prompt).
- If you accidentally exited, run docker exec -it rocky-server /bin/bash again from PowerShell.
Help me fix command not found errors in my container
You now have a clear picture of your system: Rocky Linux 9 on the RHEL platform, minimal processes, and a root-owned filesystem. Next up, you will manage the package baseline to make sure the right tools are installed and nothing unnecessary is left behind.
Establish the Package Baseline
You now know exactly what system you're working on. The next question is: what software is installed on it?
A pre-production container needs exactly the right packages installed. Missing packages break the application. Unnecessary packages expand the attack surface. In this step, you'll use DNF (the package manager for all RHEL-family systems including Rocky Linux) to establish, update, and trim the package baseline.
In this step, get ready to:
- Review the installed packages and search the repositories.
- Install and verify a network utility.
- Update all packages and remove an unnecessary one.
Review the current package baseline
Before installing or removing anything, you need to see what's already on the system. The dnf list installed command shows every package currently present in the container.
- View all currently installed packages by running this command inside the container:
dnf list installed
You should see a list of packages scrolling through the terminal. Notice how short it is compared to a full server install. This is the minimal container image at work.
- Now check whether curl is available in the package repositories by running:
dnf search curl
You should see a list of packages matching "curl" in the Rocky Linux repositories. The package named curl is the one you'll install next.
What is DNF?
DNF (Dandified Yum) is the package manager for Rocky Linux, Fedora, and RHEL 8+. It works with RPM packages and automatically resolves dependencies when you install or remove software.
If you've used apt on Ubuntu or Debian, dnf is the RHEL-family equivalent.
Install curl and verify it works
The base container image doesn't include curl. You need it for testing network connectivity and downloading resources in production containers.
- Install curl by running:
dnf -y install curl
What does the -y flag do?
The -y flag automatically answers "yes" to all prompts. Without it, dnf would pause and ask you to confirm the installation. In scripted environments and containers, you always use -y so commands can run unattended.
You should see DNF downloading and installing curl along with its dependencies, ending with "Complete!" in the output.
- Verify the installation worked by running:
curl --version
You should see the curl version number, supported protocols, and features printed to the terminal. This confirms curl is installed and functional.
Seeing "No match for argument: curl"?
This means the package repositories are not properly configured. Try running dnf makecache to refresh the repository metadata, then retry the install command.
If you see a network timeout, confirm your host machine has internet access and that Docker Desktop's network settings allow container outbound traffic.
Help me troubleshoot a dnf install failure inside my Rocky Linux container.
Update packages and trim the attack surface
A container image is a snapshot in time. By the time you pull it, some packages may already have security patches available. Updating ensures you start from the most secure baseline.
- Bring all packages to their latest versions by running:
dnf -y update
You should see DNF checking for updates and downloading newer versions. This may take a minute depending on how many packages need updating. The output ends with "Complete!" when finished.
Now you'll remove a package the container doesn't need. Every unnecessary package is extra code that could contain vulnerabilities.
- Remove the vim-minimal package (a basic text editor your container won't need in production) by running:
dnf -y remove vim-minimal
You should see DNF removing the package and printing "Complete!" when done.
- Confirm the removal by checking for any vim packages:
dnf list installed | grep vim
You should see no output at all. An empty result means no vim-related packages remain on the system.
Why remove packages from a container?
In production, every installed package is potential attack surface. If an attacker gains shell access, tools like text editors help them modify configuration files or plant backdoors.
The principle is: if the application doesn't need it to run, remove it. This is why production container images are kept as minimal as possible.
Seeing output from the grep command?
If you see a vim package still listed, the remove command may not have completed successfully. Run dnf -y remove vim-minimal again and check for error messages in the output.
Help me figure out why dnf remove didn't work in my container.
Before you move on, predict what the following command will show. Do you expect curl to appear in the installed package list?
- Run the final verification command:
dnf list installed | grep curl
You should see curl listed in the output along with its version number and the repository it came from. Your package baseline is now established: you've added what you need, updated everything to the latest patches, and removed what you don't need.
Your container now has a clean, updated package baseline. Next up, you'll create a dedicated service account so the application doesn't have to run as root.
Create a Service Account
Your package baseline is locked in. Every package on the system is accounted for, updated, and stripped of anything unnecessary.
The next question is who runs your application. Right now, everything in this container runs as root. If an attacker compromises a root-level process, they gain full control of the container and potentially the host. Production containers need a dedicated service user with limited permissions.
In this step, get ready to:
- Create an application directory at /opt/myapp.
- Create a dedicated non-root service user called appuser.
- Discover why the service user cannot write to the application directory.
Create the application directory and service user
Before your application can run, it needs a dedicated directory to live in and a dedicated user to run as. The user should be a system account, not a regular human login.
- Create the application directory and service user by running these commands:
# Create the directory where the application will live
mkdir -p /opt/myapp
# Create a system user with a home directory and bash shell
useradd -r -m -s /bin/bash appuser
What do these commands do?
- mkdir -p /opt/myapp creates the directory where your application will live. The -p flag creates parent directories as needed and does not error if the directory already exists.
- useradd -r -m -s /bin/bash appuser creates a system user called appuser. The -r flag marks it as a system account (UID below 1000). The -m flag creates a home directory. The -s /bin/bash flag sets the login shell.
- Confirm the user was created by running:
id appuser
You should see output showing a UID, GID, and groups for appuser. The UID is below 1000, confirming it is a system account.
Seeing 'user already exists'?
If you get an error that appuser already exists, that means you ran the command before. Run id appuser to confirm the user is present and continue to the next substep.
Why is my useradd command failing inside the Rocky Linux container?
Inspect the default ownership
The directory exists, but the critical question is: who owns it? On Linux, every file and directory has an owner and a group. Only the owner (or root) can write to a directory with standard permissions.
- Check who owns /opt/myapp by running:
ls -la /opt/
You should see a line like drwxr-xr-x. 2 root root ... myapp. The root root means root owns the directory and root is the group owner.
What does drwxr-xr-x mean?
- The first character d means it is a directory.
- rwx (positions 2-4) is the owner's permissions: read, write, execute. Root can do anything here.
- r-x (positions 5-7) is the group's permissions: read and execute, but no write.
- r-x (positions 8-10) is everyone else's permissions: read and execute, but no write.
This means only root can create, modify, or delete files inside /opt/myapp. Your appuser falls into the "everyone else" category.
Test the service account
You have a service user and an application directory. Time to test whether appuser can actually use it.
- Switch to the appuser account by running:
su - appuser
Your terminal prompt changes. You are no longer root.
Before you run the next command, what do you think will happen when appuser tries to create a file inside /opt/myapp?
- Try to create a file in the application directory by running:
touch /opt/myapp/test.txt
You see touch: cannot touch '/opt/myapp/test.txt': Permission denied.
This is the exact problem every production container faces. The application directory is owned by root, and your service user has no write access. This permission wall is intentional. It is the principle of least privilege at work. No user gets more access than they strictly need.
Why is Permission denied a good sign?
If appuser could write to a root-owned directory without explicit permission, that would be a security hole. The error proves that Linux permissions are working correctly.
The naive fix many teams reach for is chmod 777. That grants full read, write, and execute to every user on the system, including any attacker who gains shell access. The next step shows you the correct approach.
You have hit the permission wall. Next, you will learn the correct way to fix it without opening your system to attackers.
Harden File Permissions
You just confirmed that appuser hits "Permission denied" when writing to /opt/myapp. That's the exact wall every production container faces when a service account meets a root-owned directory.
The tempting fix is chmod 777, which makes the directory writable by everyone on the system. In this step, you'll learn the correct approach: transferring ownership with chown and applying least-privilege permission modes with chmod.
In this step, get ready to:
- Transfer directory ownership to the service user.
- Verify the service user can write to its application directory.
- Demonstrate why chmod 777 fails security audits and reset to safe permissions.
Transfer ownership to the service user
The correct fix for a root-owned directory that a service user needs to write to is changing ownership, not opening permissions to the world.
- Return to the root user by running:
exit
- Transfer ownership of the application directory to appuser by running:
chown -R appuser:appuser /opt/myapp
What does this command do?
The chown command changes file ownership. The -R flag applies the change recursively to everything inside the directory.
The format appuser:appuser sets both the owner and the group to appuser. Now appuser controls who can access these files.
- Verify the ownership change by running:
ls -la /opt/
You should see appuser appuser in the owner and group columns for the myapp directory. The permission string still reads drwxr-xr-x because you only changed who owns it, not what they can do.
Still seeing root:root?
Make sure you typed exit before running chown. If you ran it as appuser, you got an "Operation not permitted" error because only root can change ownership.
Run whoami to confirm you are root, then retry the chown command.
Help me fix the ownership of /opt/myapp
Verify appuser can write to the directory
Ownership alone isn't enough. The directory also needs the right permission mode. A mode of 755 means the owner gets read, write, and execute access, while everyone else gets read and execute only.
- Set the directory permissions explicitly by running:
chmod 755 /opt/myapp
- Switch to the service user by running:
su - appuser
Before you run the next command, think about what you expect to happen. In the previous step, touch /opt/myapp/test.txt failed with "Permission denied." Do you think it will succeed now that appuser owns the directory?
- Test creating a file in the application directory by running:
touch /opt/myapp/test.txt
No error this time. The command completes silently, which means it succeeded.
- Confirm the file exists and check its ownership by running:
ls -la /opt/myapp/
You should see test.txt listed with appuser appuser as the owner and group. The directory itself shows drwxr-xr-x (755) with appuser:appuser ownership. This is the correct production configuration.
Still getting Permission denied?
Run whoami to confirm you are logged in as appuser, not root.
If you are appuser but still get denied, the chown step may not have completed. Type exit to return to root, re-run chown -R appuser:appuser /opt/myapp, then switch back with su - appuser.
Help me debug why appuser still cannot write to /opt/myapp
Demonstrate why chmod 777 is dangerous
You fixed the problem correctly with ownership and 755. Now see what the wrong fix looks like and why it fails every security audit.
- Return to root by running:
exit
- Apply the dangerous 777 permissions to see what they look like:
chmod 777 /opt/myapp
- Check the permission string by running:
ls -la /opt/
You should see drwxrwxrwx for the myapp directory. Compare that to the safe drwxr-xr-x you had before. The difference is the last six characters: everyone now has full write and execute access.
Why does chmod 777 fail security audits?
The 777 mode means ANY user or compromised process can write to your application directory. An attacker who gains shell access can inject malicious scripts, overwrite config files, or corrupt data.
In Kubernetes, the Pod Security Standards Restricted profile explicitly prohibits privilege escalation via set-user-ID or set-group-ID file modes. Container security scanners flag world-writable paths as immediate findings.
The correct fix is always to set proper ownership with chown and use 755 or 644 modes. Never grant write access to others.
- Reset to safe permissions by running:
chmod 755 /opt/myapp
- Confirm the reset by running:
ls -la /opt/
You should see drwxr-xr-x again for myapp. The directory is back to its secure state with appuser:appuser ownership and 755 permissions.
Seeing a different permission string?
If you see something other than drwxr-xr-x, double-check the chmod 755 command. A typo like chmod 75 (missing last digit) or chmod 755 /opt/myap (wrong path) would not produce the expected result.
Help me reset /opt/myapp to 755 permissions
Your container now has a properly hardened application directory. The service user can write to its own space without world-writable permissions. Next up, you'll put these skills to the test by building an automated audit script that checks for exactly these kinds of security misconfigurations.
Secret mission
Build a Hardening Validation Script
In production, no one manually checks every file for security issues. Write a bash script that automates the hardening audit, checking for world-writable files and SUID binaries. This is the kind of artifact a Cloud Engineer adds to a CI pipeline as a pre-deployment gate.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally using Docker, so there are no ongoing cloud costs.
Resources you used:
- Docker container named rocky-server (running locally, no cost).
- Docker image rockylinux/rockylinux:9 (stored locally on disk).
Keep everything running
No action needed. Choose this if you're still actively practicing Linux commands or want to experiment further with the container.
- Your rocky-server container stays running and uses minimal system resources.
- You can re-enter it any time from PowerShell with docker exec -it rocky-server /bin/bash.
- The rockylinux/rockylinux:9 image remains cached on your machine for faster container launches in the future.
Pause - I'll come back to this later
Shut down the running container to free up memory. Your container's state is preserved and you can restart it later.
- If you are still inside the container shell, type exit to return to PowerShell.
- Stop the container by running:
docker stop rocky-server
- When you're ready to resume, restart it with:
docker start rocky-server
- Then re-enter the container with docker exec -it rocky-server /bin/bash. All your files, users, and packages will be exactly as you left them.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes the container and all changes you made inside it.
- If you are still inside the container shell, type exit to return to PowerShell.
- Remove the container (the -f flag stops it first if it is still running):
docker rm -f rocky-server
- Remove the base image to free disk space:
docker rmi rockylinux/rockylinux:9
- Confirm everything is gone by running:
docker ps -a
docker images
- You should no longer see rocky-server in the container list or rockylinux/rockylinux in the images list.
Nice Work!
Nice Work!
You just completed a production onboarding runbook against a live Rocky Linux 9 container. You now have the operational muscle memory to inspect, package, and lock down any Linux system before it reaches a Kubernetes cluster.
You've learned how to:
- Inspected a running Linux container using docker exec, cat /etc/os-release, df -h, ps aux, and ls -la to identify the system and its filesystem layout.
- Managed packages on a RHEL-family system using dnf install, dnf update, and dnf remove to establish a production-ready package baseline.
- Created a non-root service account and hardened file permissions with chown and chmod 755 instead of the dangerous chmod 777 pattern that fails security audits.
- Secret Mission: Built an automated hardening validation script that audits for world-writable files and SUID binaries.
Ready to quiz yourself?