Set Up Apache Web Server on CentOS 9
Install, configure, and secure Apache with virtual hosts and SELinux on CentOS 9.
Introduction
30 Second Summary
Every website you visit runs on a web server somewhere. Learning how to install, configure, and secure one is one of the most fundamental skills for anyone managing Linux systems.
In this project, you will set up an Apache HTTP Server on CentOS Stream 9, configure it to host multiple websites, manage the firewall, and apply security hardening.
What You'll Build
A fully configured Apache web server running on CentOS Stream 9 that serves two different websites using virtual hosts, with proper firewall rules, SELinux enforcement, and security headers.
By the end of this project, you'll have:
- A custom HTML landing page served by Apache HTTP Server on CentOS Stream 9.
- Two virtual host configurations serving different websites from the same server.
- A properly secured server with firewalld rules, SELinux enforcing, and security headers applied via .htaccess.
- Secret Mission: Configure Apache as a reverse proxy to a Python HTTP backend.
Are there any prerequisites?
You need CentOS Stream 9 running in VirtualBox with terminal access. Basic terminal familiarity (cd, ls, sudo) is expected.
Before We Start
Before We Start...
Install and Start Apache
Every website needs a web server to deliver pages to visitors. Apache HTTP Server is one of the most widely-used web servers in the world, and it comes pre-packaged for CentOS Stream 9 in the AppStream repository.
In this step, you will install Apache, turn it on, and confirm it is ready to serve web traffic.
In this step, get ready to:
- Install the httpd package using dnf.
- Enable and start the httpd service with systemctl.
- Verify that Apache is running and check the installed version.
Install the httpd package
On CentOS Stream 9, software is installed with dnf, the default package manager. The Apache web server lives in a package called httpd.
- Install Apache by running this command:
sudo dnf install httpd
What does this command do?
sudo runs the command with administrator privileges. dnf install httpd tells the package manager to download and install Apache along with any dependencies it needs.
- When dnf asks you to confirm the installation, type y and press Enter.
- You should see a list of packages being downloaded followed by a Complete! message at the end.
Not seeing Complete?
Make sure your VM has an active internet connection. Try ping google.com to confirm you are online.
If you see a permissions error, make sure you included sudo at the start of the command.
Help me troubleshoot my dnf install httpd command.
Enable and start the httpd service
Installing a package does not automatically turn it on. You need to tell systemd (the service manager on CentOS) to start the service now and also start it automatically on every future boot.
- Enable the httpd service so it starts on boot by running:
sudo systemctl enable httpd
- Start the httpd service now by running:
sudo systemctl start httpd
What is the difference between enable and start?
enable creates a link so the service launches automatically every time the system boots. start turns the service on right now, in your current session.
You need both. Without enable, Apache would stop running after your next reboot. Without start, Apache would not run until the next reboot.
Seeing a failed or inactive message?
If systemctl start httpd reports an error, another service may already be using port 80. Run sudo ss -tlnp | grep :80 to check if anything else is listening on that port.
Help me fix my httpd service that won't start.
Verify Apache is running and check the version
A running service does not always mean a healthy service. Let's confirm everything looks right.
- Check the httpd service status by running:
sudo systemctl status httpd
- Look for the line that says Active: active (running) in the output. This confirms Apache is up and accepting connections.
- Check the installed Apache version by running:
httpd -v
- You should see output that includes Server version: Apache/2.4.62.
Why version 2.4.62?
CentOS Stream 9 ships Apache 2.4.62 as a Full Life Application Stream. This means it receives security patches for the entire RHEL 9 lifecycle without needing third-party repositories.
Status shows inactive or dead?
Run sudo systemctl start httpd again, then immediately re-check with sudo systemctl status httpd.
If it still fails, check the logs by running sudo journalctl -u httpd --no-pager -n 20 to see what went wrong.
Help me understand why my httpd service is not running.
Apache is installed and running. Next up, you will open the firewall so your web server can actually receive traffic from outside.
Configure Firewalld for Web Traffic
Apache is installed and running, but right now nothing can reach it from outside the VM.
CentOS Stream 9 ships with firewalld active by default. It blocks all incoming traffic unless you explicitly allow it. In this step, you will open port 80 so that HTTP requests can reach your web server.
In this step, get ready to:
- Check the current firewall rules and identify the default zone.
- Add the HTTP service to the public zone permanently.
- Reload the firewall and verify the rule is active.
Check the current firewall rules
Before changing anything, it helps to see what the firewall currently allows. On CentOS Stream 9, firewalld assigns your network interface to the public zone by default.
- Confirm that firewalld is running by running this command:
sudo systemctl status firewalld
- You should see active (running) in the output.
- Check your default zone by running:
sudo firewall-cmd --get-default-zone
- You should see public printed to the terminal.
- List the services currently allowed in the default zone by running:
sudo firewall-cmd --list-services
- You should see ssh and dhcpv6-client listed. Notice that http is not there yet.
What is a firewall zone?
A zone is a named set of rules that firewalld applies to a network interface. The public zone is designed for untrusted networks where you only allow specific services through.
By default, firewalld blocks all incoming traffic except for the services explicitly listed in the active zone. This is why Apache cannot serve pages until you add http to the allowed list.
Add the HTTP service to the public zone
Instead of opening a raw port number, firewalld lets you allow a named service. The http service maps to TCP port 80.
- Add the http service permanently by running this command:
sudo firewall-cmd --permanent --add-service=http
What does --permanent do?
The --permanent flag writes the rule to the persistent configuration on disk. Without it, the rule would vanish the next time the firewall reloads or the system reboots.
Permanent rules do not take effect immediately. You need to reload the firewall for them to become active, which is the next step.
- You should see success printed to the terminal.
Seeing 'Authorization failed'?
Make sure you included sudo at the start of the command. Firewall changes require root privileges.
Help me fix this authorization error
Reload the firewall and verify the rule
The permanent rule is saved but not yet active. You need to reload firewalld so it reads the updated configuration.
- Reload the firewall by running:
sudo firewall-cmd --reload
- You should see success printed to the terminal.
- Verify that http is now in the allowed services by running:
sudo firewall-cmd --list-services
- You should now see http in the list alongside ssh and dhcpv6-client.
Still not seeing http in the list?
Double-check that you used the --permanent flag when adding the service. If you accidentally ran the command without it, the rule was added to runtime only and did not survive the reload. Run the add command again with --permanent and reload once more.
Help me troubleshoot my firewall rules
Your firewall is now configured to let HTTP traffic through. Next up, you will create a custom website for Apache to serve.
Serve Your First Custom Website
Your firewall is now allowing HTTP traffic through. The next piece of the puzzle is giving Apache something to actually serve.
Right now, Apache is showing its default test page. In this step, you will create your own HTML page and tell Apache exactly where to find it using a virtual host configuration.
In this step, get ready to:
- Create a custom document root directory for your site.
- Write an HTML landing page with your own content.
- Configure a virtual host to serve your new site.
Create the document root directory
A document root is the folder on disk that Apache serves files from. Instead of using the default /var/www/html directory, you will create a dedicated one for your site.
- Create the directory by running this command:
sudo mkdir -p /var/www/mysite
- Confirm the directory was created by running:
ls /var/www/
You should see mysite listed alongside the existing html directory.
Write your HTML landing page
Every website needs at least one HTML file. You will create an index.html inside your new document root so Apache has something to display.
- Open a new file in nano by running:
sudo nano /var/www/mysite/index.html
- Paste the following HTML content into the editor:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My First Apache Site</title>
</head>
<body>
<h1>Welcome to My Site</h1>
<p>This page is served by Apache HTTP Server on CentOS Stream 9.</p>
<p>Server configured and secured by me.</p>
</body>
</html>
What does this HTML do?
This is a simple static HTML page with a title, a heading, and two paragraphs. The <meta charset> and <meta viewport> tags ensure the page renders correctly across different browsers and devices.
Apache will serve this file whenever someone requests the root URL of your virtual host.
- Save the file by pressing **Ctrl+O**, then press **Enter** to confirm the filename.
- Exit nano by pressing **Ctrl+X**.
- Confirm the file was saved correctly by running:
cat /var/www/mysite/index.html
You should see the full HTML content printed in your terminal, matching what you just pasted.
Seeing a blank output or 'No such file' error?
If cat shows nothing, you may have exited nano without saving. Run the sudo nano command again, paste the content, and save with **Ctrl+O** before exiting.
If you see a 'No such file or directory' message, check that the path is exactly /var/www/mysite/index.html with no typos.
help me fix my index.html file in /var/www/mysite
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My First Apache Site</title>
</head>
<body>
<h1>Welcome to My Site</h1>
<p>This page is served by Apache HTTP Server on CentOS Stream 9.</p>
<p>Server configured and secured by me.</p>
</body>
</html>
Configure a virtual host to serve your site
A virtual host tells Apache which folder to serve for a given domain name. This is how a single server can host multiple websites.
You will create a configuration file that maps the name mysite.local to your /var/www/mysite directory.
- Open a new configuration file in nano by running:
sudo nano /etc/httpd/conf.d/mysite.conf
- Paste the following virtual host configuration:
<VirtualHost *:80>
ServerName mysite.local
DocumentRoot /var/www/mysite
<Directory /var/www/mysite>
AllowOverride All
Require all granted
</Directory>
ErrorLog /var/log/httpd/mysite-error.log
CustomLog /var/log/httpd/mysite-access.log combined
</VirtualHost>
What does this configuration do?
- ServerName mysite.local tells Apache to respond to requests for that hostname.
- DocumentRoot /var/www/mysite points Apache to the folder containing your HTML files.
- AllowOverride All lets .htaccess files in that directory override settings. You will use this later to add security headers.
- Require all granted allows all visitors to access the site.
- The ErrorLog and CustomLog lines create separate log files for this site, making troubleshooting easier.
- Save the file by pressing **Ctrl+O**, then press **Enter** to confirm.
- Exit nano by pressing **Ctrl+X**.
- Reload Apache to pick up the new configuration by running:
sudo systemctl reload httpd
- Test that your virtual host is serving your page by running:
curl -H "Host: mysite.local" http://localhost
Why use the -H flag?
Your virtual host is configured to respond to the hostname mysite.local. The -H flag sends a custom Host header in the request, telling Apache which virtual host to route to.
Without it, Apache would not know which site you are trying to reach and might show the default page instead.
You should see the full HTML from your index.html printed in the terminal, including the Welcome to My Site heading.
Not seeing your HTML page?
If you see the default Apache test page or an error, check these common causes:
- Make sure your conf file is named mysite.conf (with the .conf extension). Apache only loads files ending in .conf from the /etc/httpd/conf.d/ directory.
- Verify you ran sudo systemctl reload httpd after saving the conf file. Changes are not applied until Apache reloads.
- Check for typos in your DocumentRoot path. It must be exactly /var/www/mysite with no trailing slash.
help me troubleshoot my Apache virtual host
✔️ Awesome, I've got everything!
Great. Make sure Apache reloaded without errors and your curl output shows the HTML.
ⓧ I'd like to double check the full code
<VirtualHost *:80>
ServerName mysite.local
DocumentRoot /var/www/mysite
<Directory /var/www/mysite>
AllowOverride All
Require all granted
</Directory>
ErrorLog /var/log/httpd/mysite-error.log
CustomLog /var/log/httpd/mysite-access.log combined
</VirtualHost>
Your custom website is live. Apache is now serving your own HTML page instead of its default test page. Next up, you will add a second virtual host to serve a completely different site from the same server.
Set Up a Second Virtual Host
Your first website is live and serving content from its own document root. In production, a single Apache server almost always hosts multiple websites for different teams, clients, or projects.
Apache handles this through virtual hosts. Each virtual host maps a domain name to a different directory on disk. One Apache instance can serve completely different sites depending on which domain the visitor requests.
In this step, get ready to:
- Create a second document root with its own HTML content.
- Write a virtual host configuration for the second site.
- Test both virtual hosts using local DNS and curl.
Create the second document root and landing page
Each virtual host needs its own document root directory containing the website files it serves. Your second site will live in /var/www/secondsite.
- Create the directory for your second website by running:
sudo mkdir /var/www/secondsite
- Open a new HTML file in nano by running:
sudo nano /var/www/secondsite/index.html
- nano opens a blank file. Type or paste the following HTML content into the editor:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Second Site</title>
</head>
<body>
<h1>Welcome to the Second Site</h1>
<p>This is a completely different website served from the same Apache server.</p>
<p>Virtual hosts make multi-site hosting possible.</p>
</body>
</html>
What does this page do?
This HTML is intentionally different from your first site so you can clearly tell which virtual host Apache is serving. The title, heading, and body text are all distinct.
When you test later with curl, seeing "Welcome to the Second Site" confirms Apache is routing to the correct document root based on the requested domain name.
- Save the file by pressing Ctrl+O, then Enter to confirm the filename.
- Exit nano by pressing Ctrl+X.
- Verify the file was created by running:
cat /var/www/secondsite/index.html
You should see your full HTML content printed in the terminal, starting with <!DOCTYPE html> and ending with </html>.
Seeing "No such file or directory"?
Make sure you created the /var/www/secondsite directory with sudo mkdir before trying to open the file in nano. If you skipped the mkdir step, nano would have shown an error or created the file in the wrong location.
help me figure out why my secondsite directory or file is missing
✔️ Awesome, I've got everything!
Great. Double check you saved the file and move on to the virtual host configuration.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Second Site</title>
</head>
<body>
<h1>Welcome to the Second Site</h1>
<p>This is a completely different website served from the same Apache server.</p>
<p>Virtual hosts make multi-site hosting possible.</p>
</body>
</html>
Write the second virtual host configuration
Just like your first site, the second site needs a dedicated configuration file in /etc/httpd/conf.d/. Apache reads every .conf file in this directory and loads the virtual host definitions it finds.
- Open a new configuration file in nano by running:
sudo nano /etc/httpd/conf.d/secondsite.conf
- Type or paste the following virtual host configuration into the editor:
<VirtualHost *:80>
ServerName secondsite.local
DocumentRoot /var/www/secondsite
<Directory /var/www/secondsite>
AllowOverride All
Require all granted
</Directory>
ErrorLog /var/log/httpd/secondsite-error.log
CustomLog /var/log/httpd/secondsite-access.log combined
</VirtualHost>
How does Apache know which site to serve?
When a request arrives, Apache checks the Host header in the HTTP request and matches it against the ServerName in each virtual host block. A request for mysite.local goes to /var/www/mysite, while secondsite.local goes to /var/www/secondsite.
Both listen on *:80 (all interfaces, port 80). The domain name is the only differentiator. This is called name-based virtual hosting.
- Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit.
- Check that Apache can read the new configuration without errors by running:
sudo apachectl configtest
You should see Syntax OK in the output.
Seeing a syntax error?
Check that every opening tag has a matching closing tag. The <VirtualHost *:80> block must end with </VirtualHost>, and the <Directory> block must end with </Directory>. Also confirm there are no stray characters or missing angle brackets.
help me fix my Apache configuration syntax error
✔️ Awesome, I've got everything!
Great. Your second virtual host configuration is ready to go.
ⓧ I'd like to double check the full code
<VirtualHost *:80>
ServerName secondsite.local
DocumentRoot /var/www/secondsite
<Directory /var/www/secondsite>
AllowOverride All
Require all granted
</Directory>
ErrorLog /var/log/httpd/secondsite-error.log
CustomLog /var/log/httpd/secondsite-access.log combined
</VirtualHost>
Test both virtual hosts with local DNS
Your virtual hosts use domain names (mysite.local and secondsite.local) that don't exist in public DNS. To test them locally, you need to tell your CentOS machine that these names point to itself.
- Open the hosts file by running:
sudo nano /etc/hosts
- You will see existing entries for localhost. Add these two lines at the bottom of the file:
127.0.0.1 mysite.local
127.0.0.1 secondsite.local
What is /etc/hosts?
The /etc/hosts file is a local DNS override. Your system checks it before querying external DNS servers. By mapping mysite.local and secondsite.local to 127.0.0.1, requests to these names stay on the local machine and reach Apache.
- Save and exit nano (Ctrl+O, Enter, Ctrl+X).
- Reload Apache to pick up the new virtual host configuration by running:
sudo systemctl reload httpd
- Test the first virtual host by running:
curl http://mysite.local
You should see the HTML from your first site, including Welcome to My Site.
- Test the second virtual host by running:
curl http://secondsite.local
You should see completely different HTML, including Welcome to the Second Site. Both responses come from the same Apache server, but from different document roots.
Both URLs returning the same page?
If both curl commands return the same content, Apache is not distinguishing between the two virtual hosts. Check that ServerName in each .conf file matches the hostname you added to /etc/hosts exactly. Also confirm you ran sudo systemctl reload httpd after saving the configuration files.
help me debug why my virtual hosts are returning the same page
You now have two fully independent websites running on a single Apache server. Next up, you will tackle SELinux to make sure your custom document roots stay accessible without weakening your server's security.
Work with SELinux for Custom Document Roots
Your two virtual hosts are serving content from custom directories. Apache can read the files right now, but there is a mandatory security layer you need to configure properly.
CentOS Stream 9 runs SELinux in enforcing mode by default. SELinux controls which processes can access which files, and Apache (running as the httpd_t type) is only allowed to read files labeled with specific contexts. If your custom document roots ever lose their labels, Apache gets silently blocked.
In this step, get ready to:
- Check the current SELinux mode and confirm it is enforcing.
- Apply the correct file context to your custom document roots.
- Verify Apache can serve files with SELinux properly configured.
Check the current SELinux mode and understand why it matters
Before making any SELinux changes, you need to confirm that enforcing mode is active. In enforcing mode, SELinux actively blocks any access that violates the security policy.
- Check your SELinux mode by running this command:
getenforce
- You should see Enforcing printed in your terminal.
What does Enforcing mean?
SELinux has three modes. Enforcing means the policy is actively applied and violations are blocked. Permissive means violations are logged but not blocked. Disabled turns SELinux off entirely.
Production servers should always run in enforcing mode. Many sysadmins disable SELinux when they hit permission errors, but the correct approach is to fix the labels instead.
- Check the current file context labels on your first site by running:
ls -Z /var/www/mysite/
What does ls -Z show?
The -Z flag displays SELinux security context labels. You should see httpd_sys_content_t in the output. This is the context that tells SELinux "Apache is allowed to read this file."
Files under /var/www/ often inherit this context automatically. However, the policy database does not have a permanent rule for your custom paths. If the labels ever get reset, Apache loses access silently.
Apply the correct httpd_sys_content_t file context to your custom directories
The semanage fcontext command writes a permanent rule into the SELinux policy database. This ensures your custom directories are always labeled correctly, even after a system relabel.
- Install the package that provides semanage by running:
sudo dnf install policycoreutils-python-utils
- When prompted, type y and press Enter to confirm the installation.
Already installed?
If you see a message saying the package is already installed, that is fine. Move on to the next command.
If you see a network error, check that your VM has internet access and try again. Help me troubleshoot this dnf install error.
- Add a permanent file context rule for your first site by running:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/mysite(/.*)?"
- Add a permanent file context rule for your second site by running:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/secondsite(/.*)?"
What do these commands do?
Each command tells SELinux: "anything under this path should always be labeled httpd_sys_content_t." The -a flag adds a new rule. The -t flag specifies the type. The regex (/.*)? matches the directory and everything inside it.
This writes to the policy database only. It does not change the actual labels on disk yet.
Seeing a ValueError about file context already defined?
If the rule already exists from a previous attempt, you may see an error mentioning that the file context is already defined. This is safe to ignore. The rule is already in the policy database. You can also use the -m flag instead of -a to modify an existing rule.
Help me fix this semanage fcontext error.
Verify with restorecon and test that Apache can serve files
The policy rules are written but the actual file labels on disk need to be updated. The restorecon command reads the policy database and applies the correct labels to existing files.
- Apply the labels to your first site by running:
sudo restorecon -Rv /var/www/mysite
- Apply the labels to your second site by running:
sudo restorecon -Rv /var/www/secondsite
What do the flags mean?
The -R flag means recursive, applying the context to all files in the directory. The -v flag means verbose, printing each file that gets relabeled. If the labels were already correct, you may see no output. That is expected.
- Confirm SELinux is still enforcing and test that Apache serves your first site by running:
getenforce
curl http://mysite.local
- Test your second site by running:
curl http://secondsite.local
- You should see Enforcing from the first command, followed by the full HTML of each site. Both pages are served successfully with SELinux in enforcing mode.
Seeing a 403 Forbidden error?
A 403 from Apache with SELinux enforcing usually means the file labels are wrong. Run ls -Z /var/www/mysite/index.html and confirm you see httpd_sys_content_t in the output.
If you see a different type like default_t, run the restorecon commands again. Make sure you ran semanage fcontext first, because restorecon can only apply rules that exist in the policy.
Help me debug my SELinux 403 error with Apache.
Your server now has proper SELinux labels on both document roots. Apache can serve files without anyone having to disable SELinux. Next up, you will harden the server further by adding security headers using .htaccess files.
Harden with Security Headers and .htaccess
Your SELinux configuration is solid and both virtual hosts are serving content securely. The next layer of defense is telling browsers how to handle your content safely.
Security headers are instructions your server sends alongside every response. They prevent attacks like clickjacking and MIME-type sniffing without requiring any changes to your HTML. In this step, you'll add them using a .htaccess file.
In this step, get ready to:
- Confirm that AllowOverride and mod_headers are ready for .htaccess support.
- Create a .htaccess file with three security headers.
- Verify the headers appear in HTTP responses with curl.
Confirm AllowOverride and mod_headers are ready
A .htaccess file lets you configure Apache behavior per-directory without editing the main config files. For Apache to honor it, two things must be in place: AllowOverride All in the virtual host, and the mod_headers module loaded.
Your mysite.conf already includes AllowOverride All from the earlier virtual host setup. Confirm it is there and verify mod_headers is loaded.
- Check that AllowOverride All is present in your virtual host by running:
grep AllowOverride /etc/httpd/conf.d/mysite.conf
You should see AllowOverride All in the output.
- Verify that the headers module is loaded by running:
httpd -M | grep headers
You should see headers_module (shared) in the output.
What is .htaccess?
A .htaccess file is a per-directory configuration file that Apache reads on every request. It is commonly used in shared hosting where you don't have root access to the main server config.
On your server, it gives you a way to apply settings like security headers to specific sites without restarting Apache. The mod_headers module is what processes the Header directives you'll write inside it.
Don't see headers_module?
On CentOS Stream 9, mod_headers is included and loaded by default with the base httpd install. If it's missing, check that the httpd package installed correctly by running sudo dnf reinstall httpd.
Help me figure out why mod_headers is not showing up.
Create a .htaccess file with security headers
Now that the prerequisites are confirmed, you'll create the .htaccess file that tells browsers how to treat responses from your site.
- Create the .htaccess file in your mysite document root by running:
sudo nano /var/www/mysite/.htaccess
- Paste the following security header configuration into the file:
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
What do these headers do?
- X-Content-Type-Options: nosniff tells browsers not to guess the MIME type of a response. This prevents attacks where a malicious file is disguised as a harmless type.
- X-Frame-Options: SAMEORIGIN prevents your site from being embedded in an iframe on another domain. This blocks clickjacking attacks.
- Referrer-Policy: strict-origin-when-cross-origin controls how much URL information is sent when users click links away from your site. It sends the full URL for same-origin requests but only the domain for cross-origin ones.
- Save the file by pressing Ctrl+O, then Enter.
- Exit nano by pressing Ctrl+X.
- Confirm the file was created by running:
ls -a /var/www/mysite/
You should see .htaccess listed alongside index.html.
Don't see .htaccess in the listing?
Files starting with a dot are hidden by default. Make sure you used the -a flag with ls to show hidden files. If the file is missing entirely, re-run the sudo nano command and make sure you saved with Ctrl+O before exiting.
Help me create the .htaccess file correctly.
✔️ Awesome, I've got everything!
Great. Make sure you've saved .htaccess and you're ready to test.
ⓧ I'd like to double check the full code
Here is the complete /var/www/mysite/.htaccess file:
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Test headers with curl
The .htaccess file takes effect on the next request without a restart. However, reloading Apache ensures all configuration is cleanly applied.
- Reload Apache to ensure a clean state by running:
sudo systemctl reload httpd
- Test that the security headers are being sent by running:
curl -I http://mysite.local
You should see your three security headers in the response output: X-Content-Type-Options, X-Frame-Options, and Referrer-Policy.
What does curl -I do?
The -I flag tells curl to send a HEAD request and display only the response headers, not the page body. This is the quickest way to check whether your server is sending the correct headers without downloading the full page content.
Headers not showing up?
- Confirm AllowOverride All is set in /etc/httpd/conf.d/mysite.conf inside the <Directory> block. Without it, Apache ignores .htaccess entirely.
- Check that .htaccess is in /var/www/mysite/ (the DocumentRoot), not in a subdirectory.
- Look at the Apache error log for clues by running sudo tail /var/log/httpd/mysite-error.log.
Help me figure out why my security headers aren't appearing in curl output.
Secret mission
Configure Apache as a Reverse Proxy
In production, Apache often sits in front of application servers, routing requests to backend services running on different ports. Can you configure Apache's mod_proxy to forward requests to a Python HTTP backend on port 8080?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely inside your CentOS Stream 9 VirtualBox VM, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Apache HTTP Server (httpd) service and package.
- Virtual host config files in /etc/httpd/conf.d/ (mysite.conf, secondsite.conf, proxy.conf).
- Custom document root directories (/var/www/mysite, /var/www/secondsite, /var/www/backend).
- Firewalld http service rule in the public zone.
- SELinux file context rules and httpd_can_network_connect boolean.
- /etc/hosts entries for mysite.local, secondsite.local, and proxy.local.
- Python HTTP server process on port 8080.
Keep everything running
No action needed. Choose this if you are still actively building on your Apache server or plan to add more virtual hosts.
- Your Apache server continues serving all three virtual hosts.
- The Python backend stays running on port 8080.
- All firewall rules and SELinux settings remain active.
Pause - I'll come back to this later
Stop running processes to free up memory, but keep all files and configurations in place so you can restart later.
- Stop the Python HTTP server by finding its process and killing it:
sudo pkill -f "python3 -m http.server 8080"
- Stop and disable the Apache service:
sudo systemctl stop httpd
sudo systemctl disable httpd
- When you are ready to resume, re-enable and start Apache:
sudo systemctl enable httpd
sudo systemctl start httpd
Delete - I don't want to use this again
Remove all project resources completely. This undoes every configuration change you made during the project.
- Stop the Python HTTP server:
sudo pkill -f "python3 -m http.server 8080"
- Stop and disable Apache:
sudo systemctl stop httpd
sudo systemctl disable httpd
- Remove the virtual host config files:
sudo rm /etc/httpd/conf.d/mysite.conf
sudo rm /etc/httpd/conf.d/secondsite.conf
sudo rm /etc/httpd/conf.d/proxy.conf
- Remove the custom document root directories:
sudo rm -rf /var/www/mysite
sudo rm -rf /var/www/secondsite
sudo rm -rf /var/www/backend
- Remove the SELinux file context rules and restore defaults:
sudo semanage fcontext -d "/var/www/mysite(/.*)?"
sudo semanage fcontext -d "/var/www/secondsite(/.*)?"
- Reset the SELinux boolean for network connections:
sudo setsebool -P httpd_can_network_connect 0
- Remove the http service from the firewall:
sudo firewall-cmd --permanent --remove-service=http
sudo firewall-cmd --reload
- Remove the /etc/hosts entries you added. Open the file:
sudo nano /etc/hosts
- Delete the lines containing mysite.local, secondsite.local, and proxy.local. Save and exit nano.
- Uninstall the httpd package:
sudo dnf remove httpd
- Confirm the removal when prompted by typing y and pressing Enter.
- Optionally, delete the entire VirtualBox VM if you no longer need the CentOS Stream 9 environment. In VirtualBox Manager, right-click your VM and select Delete all files.
Nice Work!
Nice Work!
You just built a production-style Apache web server setup that mirrors what you would see in a real enterprise environment. From installation to multi-site hosting to security hardening, you covered the full lifecycle of configuring a web server on CentOS Stream 9.
You've learned how to:
- Install and configure Apache HTTP Server 2.4.62 on CentOS Stream 9, with firewalld rules to allow HTTP traffic through the public zone.
- Create two name-based virtual hosts serving different websites from the same server, tested with curl and /etc/hosts entries.
- Manage SELinux file contexts for custom document roots and apply security headers via .htaccess without ever disabling SELinux.
- Set up Apache as a reverse proxy forwarding requests to a Python HTTP backend on port 8080.
Ready to quiz yourself?