Deploy a Secure Web Server on RHEL

Deploy an Apache web server on RHEL 9 with firewall rules and SELinux.

Introduction

30 Second Summary

Every website you visit runs on a web server managed by a system administrator. Configuring these servers securely is one of the most in-demand skills in enterprise IT.

In this project, you will deploy an Apache web server on Red Hat Enterprise Linux, then lock it down with firewall rules, user permissions, and SELinux policies.

What You'll Build

A fully configured Apache HTTP web server on your RHEL 9.6 VM that serves a custom HTML page from a secured custom directory.

By the end of this project, you'll have:

  • A running Apache httpd service with a firewall rule allowing HTTP traffic on port 80.
  • A dedicated webadmin user and webdevs group with proper ownership of web content.
  • A custom document root at /srv/website with the correct SELinux context applied, proving you understand mandatory access control.
  • Secret Mission: Configure a name-based virtual host to serve a second website from a separate directory.

Are there any prerequisites?

You need RHEL 9.6 installed on VMware with root or sudo access. Your system must be registered with a Red Hat subscription so that dnf package installs work.

Install Apache and Verify the Service

Every web server deployment starts with installing the server software itself. Your RHEL 9.6 VM is ready, and the first thing you need is Apache HTTP Server, the industry-standard web server for enterprise Linux.

In this step, you'll install Apache using the dnf package manager, then start it as a systemd service. Managing packages and services are core sysadmin skills that map directly to RHCSA certification objectives.

In this step, get ready to:
  • Install the Apache HTTP Server package using dnf.
  • Start and enable the httpd service to run now and on boot.
  • Confirm the service is active and running.
Install the Apache HTTP Server package

The httpd package contains the Apache web server and all its dependencies. RHEL provides it through the AppStream repository.

  • Install the httpd package by running this command:
sudo dnf install httpd

dnf will ask for confirmation

After running this command, dnf displays a summary of packages to install and their total size. It then asks you to confirm the installation.

Type y and press Enter to proceed.

  • Verify the installation succeeded by checking the installed version:
httpd -v

You should see output showing Server version: Apache/2.4.62 (Red Hat Enterprise Linux). This confirms Apache is installed and ready to configure.

Seeing 'No match for argument: httpd'?

This means your system cannot find the package in any enabled repository. Your RHEL subscription may not be attached or the AppStream repository may be disabled.

  • Check your subscription status by running:
sudo subscription-manager status

help me fix my dnf package installation on RHEL 9

Start and enable the httpd service

Installing a package does not automatically start it on RHEL. You need to explicitly start the service and tell the system to launch it on every boot.

  • Start the httpd service and enable it to auto-start on boot by running:
sudo systemctl enable --now httpd

What does enable --now do?

The --now flag combines two actions into one command. enable configures the service to start automatically on boot. --now also starts it immediately without needing a separate systemctl start command.

If no error message appears, the command succeeded. You will confirm it is running in the next substep.

Seeing a 'port already in use' error?

Another process may already be listening on port 80. Check what is using the port by running sudo ss -tlnp | grep :80. Stop the conflicting process before trying again.

help me fix my httpd service startup issue

Check the service status
  • Confirm the service is active by running:
systemctl status httpd

Look for the line that says Active: active (running) in the output. This confirms Apache is running and ready to serve requests.

What else does the status output show?

The status output includes the service's loaded state, its main process ID (PID), memory usage, and recent log entries. You will also see enabled in the loaded line, confirming that the service will start automatically after a reboot.

Status shows inactive or failed?

  • If the status shows inactive (dead), the service was never started. Run sudo systemctl start httpd to start it manually.
  • If the status shows failed, check the logs for details by running journalctl -u httpd --no-pager. The logs usually reveal the exact issue (port conflict, configuration error, or missing files).

help me troubleshoot my httpd service

Apache is installed and running. Next up, you'll open the firewall so that HTTP traffic can actually reach your server.

Configure the Firewall

Apache is running, but it is trapped behind RHEL's firewall. firewalld blocks all inbound traffic by default unless you explicitly open a port.

In this step, you will open TCP port 80 so that HTTP requests can reach Apache. Without this rule, no browser or curl request can connect to your web server.

In this step, get ready to:
  • Inspect the current firewall rules and confirm port 80 is closed.
  • Add a permanent rule allowing HTTP traffic and reload the firewall.
  • Verify the rule is active and test the Apache default page with curl.
Check the current firewall rules

Before making changes, inspect what the firewall currently allows. This confirms that port 80 is not yet open.

  • List the active firewall rules for the default zone by running this command:
sudo firewall-cmd --list-all

What does this output show?

The output displays the default zone (usually public) and all of its settings.

Look at the ports: line. It should be empty, meaning no custom port rules exist yet. The services: line typically shows cockpit dhcpv6-client ssh on a fresh RHEL install.

  • Confirm that 80/tcp does NOT appear in the ports: line.

What is firewalld?

firewalld is RHEL's dynamic firewall manager. It groups network interfaces into zones, and each zone defines which traffic is allowed.

By default, any port you do not explicitly open is blocked. This is a core security principle: deny everything, then allow only what is needed.

Add a permanent HTTP rule and reload the firewall

Now you will open TCP port 80 permanently so the rule survives reboots. After adding a permanent rule, you must reload the firewall for it to take effect in the running session.

  • Add a permanent firewall rule for port 80 by running this command:
sudo firewall-cmd --permanent --add-port=80/tcp

What does this command do?

The --permanent flag writes the rule to disk so it persists across reboots. Without it, the rule would be active only until the next firewall reload or system restart.

The --add-port=80/tcp portion tells firewalld to allow inbound TCP connections on port 80 (the standard HTTP port).

  • You should see success printed in the terminal.
  • Reload the firewall to apply the new permanent rule by running:
sudo firewall-cmd --reload
  • You should see success again, confirming the reload completed.

Why reload separately?

Permanent rules are stored on disk but not applied to the running firewall until you reload. This two-step pattern lets you batch multiple permanent changes and apply them all at once with a single reload.

Seeing an authorization error?

Make sure you are running the command with sudo or as the root user. Firewall changes require elevated privileges.

help me fix a permission error when running firewall-cmd

Verify the rule and test the Apache page

The rule is applied. Now confirm it shows up in the active configuration and that Apache responds to requests.

  • List the active firewall rules again by running:
sudo firewall-cmd --list-all
  • Look at the ports: line. You should now see 80/tcp listed.
  • Test that Apache responds to an HTTP request by running:
curl http://localhost
  • You should see a large block of HTML output containing the default Red Hat Apache test page.

What should I see?

The curl output is the raw HTML of Apache's default test page. Look for text like "Red Hat Enterprise Linux Test Page" in the output. This confirms that Apache is serving content and the firewall is no longer blocking port 80.

Getting connection refused?

Verify that the httpd service is still running with systemctl status httpd. If it shows inactive, start it again with systemctl start httpd.

Also confirm you ran firewall-cmd --reload after adding the permanent rule. Without the reload, the port remains closed in the running session.

help me troubleshoot why curl localhost is getting connection refused on my RHEL web server

✔️ Awesome, I've got everything!

Great. Your firewall is now configured and Apache is reachable on port 80.

ⓧ I'd like to double check the full code

Here is the full sequence of commands you ran in this step:

# Check current rules (before)
sudo firewall-cmd --list-all

# Add permanent rule for HTTP on port 80
sudo firewall-cmd --permanent --add-port=80/tcp

# Reload to apply the permanent rule
sudo firewall-cmd --reload

# Verify the rule is active
sudo firewall-cmd --list-all

# Test that Apache responds
curl http://localhost

Your firewall is locked in. Next up, you will create a dedicated user and group to manage your web content securely.

Create a Web Admin User and Group

Your firewall is configured and the default Apache test page is accessible from the browser. Now it's time to think about who owns the web content files on this server.

In production, web files should never be owned by root. Instead, you create a dedicated user and group whose only job is managing web content. This limits the blast radius if the account is ever compromised.

In this step, get ready to:
  • Create a webdevs group and a webadmin user.
  • Grant webadmin sudo access via the wheel group.
  • Verify the user and group configuration.
Create the group and user

Linux uses groups to manage shared access to files. By giving your web content a dedicated group, you can later add more team members without touching individual file permissions.

  • Create the webdevs group by running this command:
sudo groupadd webdevs
  • Create the webadmin user with webdevs as their primary group by running:
sudo useradd -g webdevs webadmin

What do these commands do?

groupadd webdevs creates a new group entry in /etc/group. This group will own all web content files later.

useradd -g webdevs webadmin creates a new user whose primary group is webdevs instead of the default auto-generated private group. The -g flag sets the primary group.

  • Confirm the user was created by running:
id webadmin

You should see output showing webadmin with webdevs as the primary group.

Seeing 'group webdevs does not exist'?

Make sure you ran the groupadd command before useradd. The group must exist before you can assign a user to it.

If you accidentally ran them out of order, delete the user with sudo userdel webadmin and start again from the groupadd step.

help me fix my useradd group error

Grant sudo access and set a password

The wheel group on RHEL grants members full sudo privileges. Adding webadmin to this group lets the account perform administrative tasks when needed.

  • Add webadmin to the wheel group by running:
sudo usermod --append -G wheel webadmin

What does this command do?

usermod --append -G wheel webadmin adds wheel as a supplementary group without removing the user from webdevs. The --append flag is critical. Without it, the command replaces all existing supplementary groups.

  • Set a password for webadmin by running:
sudo passwd webadmin

What should I see?

The passwd command prompts you to type a new password twice. Characters will not appear on screen as you type. Choose any password you will remember for this lab session.

Password not meeting complexity requirements?

RHEL may warn that the password is too simple, but as root or sudo it will still accept it after the warning. If the command fails, make sure you are running it with sudo.

help me set a password for my webadmin user

Verify the user and group configuration
  • Check the full group membership for webadmin by running:
id webadmin

You should see output similar to this:

uid=1001(webadmin) gid=1001(webdevs) groups=1001(webdevs),10(wheel)

How do I read this output?

uid is the user's unique numeric ID. gid shows the primary group, which is webdevs.

The groups field lists all groups the user belongs to. You should see both webdevs and wheel listed here.

Only seeing webdevs but not wheel?

Double check that you included the --append flag in the usermod command. Without it, the command replaces existing groups instead of adding to them.

Run the usermod --append -G wheel webadmin command again and then re-check with id webadmin.

help me fix my webadmin group membership

Your dedicated web admin account is ready. Next, you will create a custom document root directory and assign proper ownership so webadmin controls the web content.

Set Up a Custom Document Root with Proper Permissions

Your webadmin user and webdevs group are ready to own web content. Now you need a directory for that content to live in.

Apache's default document root is /var/www/html. You'll create a custom directory at /srv/website instead, which sets up a real SELinux challenge you'll solve in the next step.

In this step, get ready to:
  • Create the /srv/website directory and a custom HTML page.
  • Set file ownership and permissions for secure access.
  • Configure Apache to serve content from the new directory.
Create the directory and custom HTML page

Why not use the default /var/www/html?

The default /var/www/html directory already has the correct SELinux context applied automatically. Using a custom path at /srv/website forces you to encounter and resolve a real SELinux denial.

This is exactly the scenario tested on the RHCSA exam and something you'll face when configuring production servers with non-default paths.

  • Create the /srv/website directory by running this command:
sudo mkdir -p /srv/website
  • Confirm the directory exists by running:
ls -ld /srv/website

You should see a line showing the directory with root ownership and drwxr-xr-x permissions.

  • Create a custom index.html file inside /srv/website by running this command:
sudo tee /srv/website/index.html > /dev/null << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My RHEL Web Server</title>
</head>
<body>
    <h1>Welcome to My RHEL Web Server</h1>
    <p>This page is served by Apache on Red Hat Enterprise Linux 9.6.</p>
    <p>Document root: /srv/website</p>
</body>
</html>
EOF

What does this command do?

The tee command writes input to a file. Using sudo tee lets you write to a root-owned location because normal shell redirects like > happen before sudo takes effect.

The << 'EOF' part is called a heredoc. Everything between EOF markers gets passed as input to tee. The > /dev/null suppresses the echo to your terminal.

  • Verify the file was created correctly by running:
cat /srv/website/index.html

You should see the full HTML content you just wrote, starting with <!DOCTYPE html> and ending with </html>.

Seeing "No such file or directory"?

Make sure the mkdir command ran successfully before you tried to create the file. Run ls -ld /srv/website to confirm the directory exists.

If you see a permission denied error, make sure you included sudo at the start of the command.

help me fix a file creation error in /srv/website

Set ownership and permissions

The directory is currently owned by root. You need to transfer ownership to webadmin:webdevs so your web admin user can manage the content.

  • Set ownership of the directory and its contents by running this command:
sudo chown -R webadmin:webdevs /srv/website
  • Set the correct permissions on the directory and file by running these commands:
# 755 = owner can read/write/execute, group and others can read/execute
sudo chmod 755 /srv/website

# 644 = owner can read/write, group and others can read only
sudo chmod 644 /srv/website/index.html

Why these permission numbers?

Directories need the execute bit (755) so users can traverse into them. Files only need read access (644) for Apache to serve them.

The Apache process needs to be able to read the files. With 644 permissions, the "others" read bit ensures Apache can access the content regardless of which user it runs as.

  • Verify the ownership and permissions by running:
ls -la /srv/website/

You should see webadmin webdevs as the owner and group for both entries. The directory should show drwxr-xr-x and index.html should show -rw-r--r--.

Seeing root as owner instead of webadmin?

Make sure you ran the chown command with the -R flag to apply it recursively. Without -R, only the directory itself changes ownership while the file inside stays owned by root.

help me fix ownership on /srv/website

Configure Apache to use the new document root

Apache still points to its default document root. You need to tell it about your new /srv/website directory by creating a configuration file.

  • Create the Apache configuration file by running this command:
sudo tee /etc/httpd/conf.d/website.conf > /dev/null << 'EOF'
<VirtualHost *:80>
    ServerName localhost
    DocumentRoot "/srv/website"

    <Directory "/srv/website">
        AllowOverride None
        Require all granted
    </Directory>
</VirtualHost>
EOF

What does this configuration do?

  • VirtualHost *:80 tells Apache this config applies to all HTTP requests on port 80.
  • DocumentRoot "/srv/website" points Apache to your custom directory instead of the default /var/www/html.
  • Require all granted allows all clients to access the content in this directory.
  • AllowOverride None prevents .htaccess files from changing server behavior, keeping your config centralized.
  • Verify the config file looks correct by running:
cat /etc/httpd/conf.d/website.conf

You should see the full VirtualHost block with DocumentRoot "/srv/website" and the Directory section.

  • Restart Apache to load the new configuration by running:
sudo systemctl restart httpd
  • Confirm the service restarted successfully by running:
systemctl status httpd

You should see active (running) in green text. If httpd failed to start, there's a syntax error in your configuration file.

httpd failed to restart?

If the service shows failed status, check for syntax errors in your config file. A missing closing tag, unclosed quote, or typo in a directive name will prevent Apache from starting.

Run sudo httpd -t to test the configuration syntax. Apache will tell you the exact line with the error.

help me fix my Apache configuration syntax error

✔️ Awesome, I've got everything!

Great. Double check you see active (running) in the httpd status output, then move on.

ⓧ I'd like to double check the full code

Here is the complete /etc/httpd/conf.d/website.conf file:

<VirtualHost *:80>
    ServerName localhost
    DocumentRoot "/srv/website"

    <Directory "/srv/website">
        AllowOverride None
        Require all granted
    </Directory>
</VirtualHost>

And here is the complete /srv/website/index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My RHEL Web Server</title>
</head>
<body>
    <h1>Welcome to My RHEL Web Server</h1>
    <p>This page is served by Apache on Red Hat Enterprise Linux 9.6.</p>
    <p>Document root: /srv/website</p>
</body>
</html>

Your custom document root is in place and Apache knows about it. But if you try to load the page right now, you'll hit a wall. In the next step, you'll discover why SELinux blocks access to your new directory and learn how to fix it.

Fix SELinux for the Custom Directory

Your Apache configuration, file permissions, and firewall are all correct. Yet when you try to load the page, you get a 403 Forbidden error.

The culprit is SELinux. Because your document root lives at /srv/website instead of the default /var/www/html, SELinux blocks Apache from reading it. In this step, you will fix the SELinux context so Apache can serve your custom content.

In this step, get ready to:
  • Observe the SELinux denial by requesting your page with curl.
  • Apply the correct SELinux context using semanage fcontext and restorecon.
  • Verify the fix and confirm your website loads successfully.
Observe the SELinux denial

Before fixing anything, confirm the problem is real. Apache is running and the firewall is open, but SELinux is preventing access to /srv/website.

  • Request the page from Apache by running this command:
curl http://localhost

You should see a response that includes 403 Forbidden. This means Apache received the request but was denied permission to read your files.

Why is Apache blocked when permissions look fine?

SELinux is a Mandatory Access Control system that runs on top of standard Linux file permissions. Even though webadmin owns the files and the permission bits allow reading, SELinux has its own set of labels (called contexts) that determine which processes can access which files.

The Apache process runs as type httpd_t. It can only read files labeled with httpd_sys_content_t. Files under /var/www/ get this label automatically, but files under /srv/ do not.

  • Confirm the current SELinux context on your directory by running:
ls -ldZ /srv/website

You should see a context type like var_t in the output. This is the wrong type for web content.

Not seeing 403 Forbidden?

If you see the HTML content instead of a 403, SELinux may not be in enforcing mode. Run getenforce to check. It should return Enforcing. If it says Permissive, SELinux is logging denials but not blocking them.

help me figure out why I'm not seeing a 403 Forbidden from SELinux

Apply the correct SELinux context

To fix the denial, you need to tell SELinux that /srv/website and everything inside it should be labeled as web content. This is a two-step process: define the rule, then apply it to the files on disk.

  • Install the policycoreutils-python-utils package, which provides the semanage and restorecon commands, by running:
sudo dnf install policycoreutils-python-utils -y

What is policycoreutils-python-utils?

This package provides the semanage utility for managing SELinux policy rules from the command line. Without it, you cannot add custom file context definitions. It is part of the standard RHEL 9 SELinux tooling.

  • Add a file context rule that labels /srv/website and all its contents as httpd_sys_content_t by running:
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/website(/.*)?"

What does this command do?

  • semanage fcontext manages the SELinux file context database.
  • -a adds a new rule.
  • -t httpd_sys_content_t specifies the SELinux type that Apache is allowed to read.
  • "/srv/website(/.*)?" is a regular expression matching the directory and everything inside it.
  • Apply the new context rule to the actual files on disk by running:
sudo restorecon -Rv /srv/website/

Why do you need restorecon?

The semanage fcontext command only records the rule in SELinux's policy database. The files on disk still have their old labels. The restorecon command walks the file tree and relabels each file to match the policy.

The -R flag makes it recursive. The -v flag prints each file it relabels so you can confirm the change.

You should see output showing the relabeling of /srv/website and /srv/website/index.html to httpd_sys_content_t.

Seeing "command not found" for semanage?

Make sure you installed the policycoreutils-python-utils package in the previous command. Run which semanage to confirm it is available. If the install failed, check that your RHEL subscription is active with sudo subscription-manager status.

help me troubleshoot why semanage is not found on my RHEL system

Verify the context and load the website
  • Confirm the SELinux context is now correct by running:
ls -ldZ /srv/website

You should see httpd_sys_content_t in the context field. This confirms SELinux will allow Apache to read these files.

  • Test that your website now loads successfully by running:
curl http://localhost

You should see the HTML content of your custom page, including the heading Welcome to My RHEL Web Server.

What should I see?

The curl output should display your full HTML page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My RHEL Web Server</title>
</head>
<body>
    <h1>Welcome to My RHEL Web Server</h1>
    <p>This page is served by Apache on Red Hat Enterprise Linux 9.6.</p>
    <p>Document root: /srv/website</p>
</body>
</html>

Still seeing 403 Forbidden?

Run ls -ldZ /srv/website/index.html and make sure the file also shows httpd_sys_content_t. If it still shows the old context, run restorecon -Rv /srv/website/ again.

Also confirm Apache was restarted after the configuration change in the previous step. Run systemctl restart httpd and try curl http://localhost again.

help me fix my 403 Forbidden error after applying SELinux context

Secret mission

Add a Virtual Host for a Second Website

In production, a single Apache server often hosts multiple websites. Can you configure a name-based virtual host to serve a second site from a completely separate directory?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local RHEL 9.6 VM, so there are no ongoing cloud costs. Decide whether to keep your web server running, pause it to come back later, or delete everything entirely.

Resources you used:

  • Apache httpd service (installed package, running and enabled).
  • Firewall rule allowing HTTP traffic on port 80.
  • User webadmin and group webdevs.
  • Directories /srv/website and /srv/website2 with custom HTML files.
  • Apache configuration files in /etc/httpd/conf.d/.
  • SELinux fcontext rules for /srv/website and /srv/website2.
  • Local /etc/hosts entry for site2.example.local.

Keep everything running

No action needed. Choose this if you are still actively building on your web server or plan to continue experimenting with Apache, SELinux, and virtual hosts.

  • Your httpd service will continue running and auto-start on boot.
  • Port 80 remains open in your firewall.
  • The webadmin user and webdevs group remain on the system.

Pause - I'll come back to this later

Stop the httpd service and close port 80 so your VM is not actively serving traffic. Your files and configuration stay on disk for when you return.

  • Stop and disable the httpd service:
sudo systemctl stop httpd
sudo systemctl disable httpd
  • Remove the firewall rule for port 80 and reload:
sudo firewall-cmd --permanent --remove-port=80/tcp
sudo firewall-cmd --reload

When you are ready to resume, re-enable and start httpd, then re-add the firewall rule.

Delete - I don't want to use this again

Remove all project resources from your VM and start fresh. Run these commands as root or with sudo.

  • Stop and disable the httpd service:
sudo systemctl stop httpd
sudo systemctl disable httpd
  • Remove the Apache package:
sudo dnf remove httpd -y
  • Remove the firewall rule for port 80 and reload:
sudo firewall-cmd --permanent --remove-port=80/tcp
sudo firewall-cmd --reload
  • Delete the web content directories:
sudo rm -rf /srv/website
sudo rm -rf /srv/website2
  • Remove the Apache configuration files:
sudo rm -f /etc/httpd/conf.d/website.conf
sudo rm -f /etc/httpd/conf.d/website2.conf
  • Remove the SELinux fcontext rules and restore default contexts:
sudo semanage fcontext -d "/srv/website(/.*)?"
sudo semanage fcontext -d "/srv/website2(/.*)?"
sudo restorecon -Rv /srv/
  • Delete the webadmin user and webdevs group:
sudo userdel -r webadmin
sudo groupdel webdevs
  • Remove the /etc/hosts entry for site2.example.local. Open the file in a text editor and delete the line containing 127.0.0.1 site2.example.local:
sudo sed -i '/site2.example.local/d' /etc/hosts

Nice Work!

Nice Work!

You just deployed a production-style web server on Red Hat Enterprise Linux 9.6. Your RHEL VM is now serving custom web content through a properly secured Apache configuration with enterprise-grade access controls.

You've learned how to:

  • Installed and enabled the Apache HTTP Server using dnf and systemctl, and configured firewalld to allow HTTP traffic on port 80.
  • Created a dedicated webadmin user and webdevs group with proper file ownership and permissions over a custom document root.
  • Configured SELinux to allow Apache to serve content from a non-default directory using semanage fcontext and restorecon.
  • Secret Mission: Configured name-based virtual hosting to serve multiple websites from one server.

Ready to quiz yourself?