Signal9Signal9 — home
online
all lessons

LPIC-2 HTTP Services: Apache, TLS, and Nginx Reverse Proxy

Advanced40 min7 steps
What you'll learn
  • Host several sites on one Apache server with name-based virtual hosts and protect them with htpasswd
  • Terminate HTTPS in Apache by wiring up a certificate, private key, and chain — and explain why the name on the cert has to match
  • Turn Nginx into a reverse proxy and load balancer with upstream and proxy_pass
  • Preserve the real client's identity to a backend with the Host and X-Forwarded-For headers
  • Make validation-before-reload a reflex so a typo never takes a live site offline
  1. Why one server hosts many sites

    Understand the problem virtual hosts solve and the vocabulary used throughout.

    A single machine, with a single IP address, almost always serves more than one website. shop.example.com and blog.example.com can live on the same box, the same port 443, even the same process. The web server has to decide which site a request is for and hand back the right content.

    The mechanism is the virtual host (vhost). Think of the server as a receptionist at a shared office building: many companies sit behind one front desk and one street address. When you walk in you say who you're here to see — "I'm here for Acme" — and the receptionist routes you to the right floor. On the web, the visitor states who they want in the HTTP `Host:` header (and, for HTTPS, in the TLS SNI field), and the server matches that against each vhost's configured name.

    Apache calls this block a <VirtualHost> and matches on `ServerName`. Nginx calls it a server {} block and matches on `server_name`. Same idea, two dialects.

  2. Apache name-based virtual hosts

    Define two sites in Apache and route between them by ServerName.

    Each site gets its own <VirtualHost> block. The key line is `ServerName` — that's the name Apache compares against the incoming Host: header. DocumentRoot says where that site's files live. Below, two sites share one address and port; the name is the only thing that tells them apart.

    Example — not runnable on your lab box

    cat /etc/apache2/sites-available/shop.conf
    <VirtualHost *:80>
        ServerName shop.example.com
        DocumentRoot /var/www/shop
    </VirtualHost>
    
    <VirtualHost *:80>
        ServerName blog.example.com
        DocumentRoot /var/www/blog
    </VirtualHost>
    Both blocks listen on *:80 — the same IP and port. Apache picks between them purely by matching the request's Host: header against each ServerName. A request for shop.example.com is served from /var/www/shop; one for blog.example.com from /var/www/blog. The first vhost listed also becomes the default for any request whose name matches nothing.

    Check yourself

    Two <VirtualHost *:80> blocks listen on the same IP and port. What does Apache use to decide which one serves a given request?

  3. Validate, then reload — never the other way around

    Build the habit that keeps a config typo from taking the site down.

    You changed a config. Before you make Apache re-read it, prove the file parses. Apache won't start — or restart — with a broken config, so reloading a syntax error on a running server can drop the site entirely until you fix it. The fix is one cheap command first.

    Example — not runnable on your lab box

    apachectl configtest
    Syntax OK
    configtest
    Parse every config file and report errors WITHOUT touching the running server. Same as apachectl -t / httpd -t.
    Syntax OK is your green light. If you'd left a missing closing </VirtualHost> or a typo'd directive, you'd see the exact file and line number here instead — and the running server would still be happily serving the old, good config because nothing has reloaded yet.

    Example — not runnable on your lab box

    sudo apachectl graceful
    graceful
    Re-read config and restart workers AFTER they finish their current request — no dropped connections.
    restart
    Hard restart: kills workers immediately, interrupting in-flight requests. Avoid on a busy site.
    graceful tells each Apache worker to finish the request it's handling, then pick up the new config. Active visitors never see a blip. Pair configtest then graceful and a deploy becomes boringly safe — which is exactly what you want from a deploy. Silent on success — a graceful reload finishes in-flight requests on the old workers before swapping in the new config, so live connections never drop. apachectl configtest first, always.
  4. Password-protect a site with htpasswd

    Add HTTP Basic Auth — and avoid the flag that silently wipes every user.

    The simplest way to gate a directory is HTTP Basic Auth backed by an .htpasswd file: a list of usernames and hashed passwords. You create entries with the htpasswd tool, then point a vhost or <Directory> at the file with AuthType Basic and AuthUserFile.

    The very first user is special — the file doesn't exist yet, so you have to create it. After that, the file exists and you only add to it.

    Example — not runnable on your lab box

    sudo htpasswd -c /etc/apache2/.htpasswd alice
    New password:
    Re-typing new password:
    Adding password for user alice
    -c
    CREATE the file. Use this ONCE, for the very first user only. It OVERWRITES any existing file.
    -c creates a fresh .htpasswd with alice as the sole entry. The password is prompted for (never passed on the command line, where it'd land in your shell history) and stored hashed, not in plaintext.

    Example — not runnable on your lab box

    sudo htpasswd /etc/apache2/.htpasswd bob
    New password:
    Re-typing new password:
    Adding password for user bob
    No `-c` this time. The file already exists, so we just append bob. Now both alice and bob are in the file. This is the command you'll use for every user after the first.

    Check yourself

    Your .htpasswd already has five users. You run htpasswd -c .htpasswd newuser. How many users are in the file now?

  5. Serve it over HTTPS (TLS)

    Wire up a cert, key, and chain — and see why the cert's name must match ServerName.

    Turning a site to HTTPS means terminating TLS. Apache's mod_ssl needs three things and one switch:

    • `SSLCertificateFile` — the server's public certificate (proves identity, signed by a CA)
    • `SSLCertificateKeyFile` — the matching private key (kept secret; mathematically paired to the cert)
    • `SSLCertificateChainFile` — the intermediate chain linking your cert up to a root the browser trusts
    • `SSLEngine on` — the switch that actually enables TLS for this vhost

    Think of it as a passport at border control: the certificate is the passport (it states a name and is stamped by an authority), the private key proves you're its real holder, and the chain is the issuing-authority paperwork the officer trusts.

    Example — not runnable on your lab box

    cat /etc/apache2/sites-available/shop-ssl.conf
    <VirtualHost *:443>
        ServerName shop.example.com
        DocumentRoot /var/www/shop
    
        SSLEngine on
        SSLCertificateFile      /etc/ssl/certs/shop.example.com.crt
        SSLCertificateKeyFile   /etc/ssl/private/shop.example.com.key
        SSLCertificateChainFile /etc/ssl/certs/chain.pem
    </VirtualHost>
    This is the HTTPS twin of the port-80 vhost, now on *:443 with TLS turned on. The cert and key are a matched pair — the key must be the one the cert was issued for, or configtest fails with a key-mismatch error. Keep the key under /etc/ssl/private/ with tight permissions; it's the one secret that, if leaked, breaks everything.

    Check yourself

    A browser visiting https://shop.example.com gets a certificate error even though Apache is running and SSLEngine is on. What's the most likely cause?

  6. Nginx as a reverse proxy and load balancer

    Use upstream and proxy_pass to put Nginx in front of backend apps — and pass the client through.

    Nginx's server {} block is its vhost, matched by server_name — the direct analogue of Apache's <VirtualHost>/ServerName. But Nginx's signature job is the reverse proxy: instead of serving files itself, it forwards requests to one or more backend application servers and relays their responses.

    List the backends in an `upstream {}` block, then proxy_pass to that group. With more than one backend, Nginx spreads requests across them — that's load balancing for free. Picture Nginx as the building's receptionist again, but now also a dispatcher: it takes every visitor and sends them to whichever caseworker (backend) is free.

    Example — not runnable on your lab box

    cat /etc/nginx/conf.d/shop.conf
    upstream shop_backends {
        server 192.0.2.21:8080;
        server 192.0.2.22:8080;
    }
    
    server {
        listen 80;
        server_name shop.example.com;
    
        location / {
            proxy_pass http://shop_backends;
            proxy_set_header Host              $host;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        }
    }
    The upstream block names a pool of two app servers. The server block is the vhost for shop.example.com; proxy_pass http://shop_backends hands matching requests to the pool, alternating between the two backends by default (round-robin). Nginx is now a load balancer — add or remove server lines to scale the pool.

    Round-robin assumes every backend is equal and always wanted. Two upstream parameters cover the cases it doesn't:

    • `backup` marks a server as standby — Nginx sends it no traffic while any non-backup server in the pool is up, and falls back to it only when they are all down. That is how you keep a spare out of rotation until it is actually needed.
    • Automatic failover is already on: if a request to one backend fails to connect, Nginx retries it on the next server (that is the default proxy_next_upstream error timeout), so one dead primary never reaches the user as an error. max_fails= and fail_timeout= control how many failures mark a server down and for how long.

    Example — not runnable on your lab box

    cat /etc/nginx/conf.d/shop.conf
    upstream shop_backends {
        server 192.0.2.21:8080;            # primary
        server 192.0.2.22:8080;            # primary
        server 192.0.2.23:8080 backup;    # standby — only used when both primaries are down
    }
    The first two servers share traffic round-robin; 192.0.2.23 sits idle with backup until both primaries are unreachable, then it serves everything. Because failover is automatic, stopping one primary just shifts its share to the other with no error reaching the client — exactly what a load balancer in front of app servers is for.

    Example — not runnable on your lab box

    nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
    -t
    Test the config and exit — validate WITHOUT reloading. Nginx's apachectl-configtest equivalent.
    Same discipline as Apache: prove it parses before you reload. Only after test is successful do you run sudo nginx -s reload (or systemctl reload nginx) to apply it gracefully. Validate, then reload — every time, for every server.

    Check yourself

    After putting Nginx in front of an app, the backend's logs show every request coming from Nginx's IP instead of real visitors. Which header fixes this?

  7. Try it in your live terminal

    Do it, don't just read it — these safe, read-only checks run on the real sandbox to your right.

    You have a real shell. These commands are read-only — they report what web software is present and whether anything is answering on port 80, without changing a thing. Run them in order to orient yourself the way you would on an unfamiliar server.

    apachectl -v 2>/dev/null || httpd -v 2>/dev/null || echo 'no apache'
    Is Apache installed, and which version? It tries the Debian name (apachectl) then the RHEL name (httpd), and prints no apache if neither exists — so the check works on any distro and never errors out.
    nginx -v 2>&1 || echo 'no nginx'
    Same question for Nginx. nginx -v prints its version to stderr, so 2>&1 makes sure you see it; || echo 'no nginx' covers the case where Nginx isn't installed.
    curl -sI http://localhost 2>/dev/null | head -1
    Is anything actually serving HTTP here and now? -sI does a silent HEAD request (headers only, no body); head -1 keeps just the status line, e.g. HTTP/1.1 200 OK. Empty output means nothing is listening on port 80 — itself a useful finding.
Recap
  • Name-based virtual hosts let one IP serve many sites: Apache matches ServerName, Nginx matches server_name, both against the client's requested name.
  • Validate before you reload — every time. apachectl configtest / nginx -t first, then apachectl graceful / reload. A typo reloaded onto a live server takes the site down.
  • htpasswd -c creates (and overwrites) the password file — use it once, ever. Drop -c to add users; re-running it wipes everyone already in the file.
  • TLS needs cert + key + chain with SSLEngine on, and the cert's CN/SAN must match ServerName or the browser rejects it. SNI lets many HTTPS vhosts share one IP.
  • Nginx upstream {} + proxy_pass turn it into a reverse proxy / load balancer — and you must forward Host and X-Forwarded-For so the backend sees the real site and the real client.
Directive / command → effect
Directive or commandServerWhat it doesWatch for
ServerName / server_nameApache / NginxRoutes a request to a vhost by the name the client asked forMust match the Host: header
apachectl configtest / nginx -tApache / NginxParse-checks config without reloadingRun it BEFORE any reload
apachectl gracefulApacheReloads config, finishing in-flight requests firstNo-downtime reload; never reload an untested config
htpasswd -c file userApacheCreates the password file with one userOVERWRITES the file — use once only
htpasswd file userApacheAppends a user to an existing fileThe safe way to add more users
SSLEngine on + cert/key/chainApacheTerminates HTTPS for the vhostCert CN/SAN must match ServerName
upstream {} + proxy_passNginxReverse-proxies / load-balances to backend serversForward Host and X-Forwarded-For