Password protection in Apache 2.4: Basic, Digest and LDAP

Updated November 2026

To put a password on part of a site, create a password file with htpasswd -B, add an AuthType Basic block with Require valid-user, and serve that area only over HTTPS. This page shows each step for Apache httpd 2.4, then groups, Digest (and why to skip it) and logins against LDAP or Active Directory.

Old series, current syntax. Apache Today ran "Apache Authentication, Parts 1–4" in July and August 2000 (see the 2000 archive). The ideas still hold, but the directives changed in 2.4. Every example below was tested on Apache httpd 2.4 in November 2026.

How HTTP authentication works

The first request for a protected page gets a 401 Unauthorized answer with a WWW-Authenticate header naming the realm (the text you set with AuthName). The browser shows a login box and sends the request again with an Authorization header. It then repeats that header on every later request to the same area, including each image and stylesheet.

Browser Apache httpd 1. GET /private/ 2. 401 + WWW-Authenticate: Basic realm="Private area" 3. GET /private/ + Authorization: Basic YWxpY2U6… 4. 200 OK (or 401 again if the password is wrong)
Basic authentication: the password travels in step 3, and again with every later request.

With Basic authentication the header is only the user name and password joined by a colon and Base64-encoded. Base64 is not encryption; anyone who sees the header can reverse it:

$ echo 'YWxpY2U6UzNjcmV0LWFsaWNl' | base64 -d
alice:S3cret-alice

HTTPS only. The Apache documentation says Basic authentication sends the password unencrypted and should not protect sensitive data unless the connection uses mod_ssl. Put the protected area in your HTTPS virtual host and redirect plain HTTP to HTTPS (HTTPS guide, redirects).

The modules involved

Apache 2.4 splits the job into small modules:

Debian and Ubuntu enable auth_basic, authn_core, authn_file, authz_core, authz_host and authz_user by default; groups, Digest and LDAP need a2enmod. RHEL, Alma, Rocky and Fedora load all the core auth modules from /etc/httpd/conf.modules.d/00-base.conf. Check with apachectl -M | grep auth.

Step 1: create the password file

The htpasswd tool comes in apache2-utils on Debian/Ubuntu and httpd-tools on RHEL/Fedora:

sudo apt install apache2-utils      # Debian, Ubuntu
sudo dnf install httpd-tools        # RHEL, Alma, Rocky, Fedora

Create the file with the first user. -c creates the file, -B selects bcrypt and -C sets the bcrypt cost. You are asked for the password twice:

sudo htpasswd -c -B -C 10 /etc/apache2/.htpasswd alice     # RHEL: /etc/httpd/.htpasswd
New password:
Re-type new password:
Adding password for user alice

Add more users without -c; with it, an existing file is truncated and every other user is lost:

sudo htpasswd -B -C 10 /etc/apache2/.htpasswd bob
sudo htpasswd -v /etc/apache2/.htpasswd bob      # check a password
sudo htpasswd -D /etc/apache2/.htpasswd bob      # delete a user

Each line is name:hash, for example alice:$2y$10$…. Keep the file outside the DocumentRoot and readable by the Apache user only:

sudo chown root:www-data /etc/apache2/.htpasswd   # RHEL: root:apache /etc/httpd/.htpasswd
sudo chmod 640 /etc/apache2/.htpasswd

For scripts, pipe the password in with -i rather than -b, which puts it on the command line where other users and shell history can see it: printf '%s\n' "$PW" | sudo htpasswd -i -B -C 10 /etc/apache2/.htpasswd carol.

Which hash to use

htpasswd hash options (Apache httpd 2.4)
OptionHashVerdict
-BbcryptUse this. Cost 4–17, default 5.
-5 / -2SHA-512 / SHA-256 cryptAcceptable; default 5,000 rounds (-r).
-mApache MD5 ("apr1")The default if you give no option. Weak today.
-sSHA-1, unsaltedInsecure.
-dcrypt()Insecure; uses only the first 8 characters.
-pplain textOnly accepted on Windows and NetWare. Never.

The cost matters more than you might expect, because Apache checks the hash on every request, not once per login. In our test (httpd 2.4.69, one request at a time), a request took about 10 ms at cost 5, 65 ms at cost 10 and 240 ms at cost 12, against under 1 ms without a password; your hardware will differ. A page with 20 images at cost 12 adds seconds of CPU time. Cost 10 is a reasonable middle. Turning on mod_authn_socache did not help in our test, and its documentation says file lookups are unlikely to benefit.

Step 2: protect a directory or URL

Put the block in the main configuration, inside your HTTPS <VirtualHost *:443> (Debian: /etc/apache2/sites-available/; RHEL: /etc/httpd/conf.d/):

<Directory "/var/www/example.com/private">
    AuthType Basic
    AuthName "Private area"
    AuthBasicProvider file
    AuthUserFile "/etc/apache2/.htpasswd"
    Require valid-user
</Directory>

Use <Location "/admin"> instead when the URL is produced by an application or a proxy rather than a folder on disk. Replace Require valid-user with Require user alice bob to allow only named users. Then test the configuration and reload:

sudo apache2ctl configtest && sudo systemctl reload apache2   # Debian, Ubuntu
sudo apachectl configtest && sudo systemctl reload httpd      # RHEL, Fedora

If you cannot edit the main configuration, the same lines (without the <Directory> wrapper) work in a .htaccess file, provided the server allows AllowOverride AuthConfig for that folder. See the .htaccess guide for the costs.

Groups of users

A group file is plain text, one group per line, members separated by spaces:

# /etc/apache2/.htgroups
editors: alice
viewers: alice bob
sudo a2enmod authz_groupfile && sudo systemctl restart apache2   # Debian, Ubuntu only

<Directory "/var/www/example.com/drafts">
    AuthType Basic
    AuthName "Editors only"
    AuthUserFile "/etc/apache2/.htpasswd"
    AuthGroupFile "/etc/apache2/.htgroups"
    Require group editors
</Directory>

Bob's password is correct, but he is not an editor, so he gets 401 and the error log records AH01631: user bob: authorization failure.

Password or trusted network

Several Require lines in one section mean "any of these". This lets the office network in without a password and asks everyone else to log in (the 2.4 replacement for the old Satisfy Any):

<Directory "/var/www/example.com/reports">
    AuthType Basic
    AuthName "Reports"
    AuthUserFile "/etc/apache2/.htpasswd"
    <RequireAny>
        Require ip 192.0.2.0/24
        Require valid-user
    </RequireAny>
</Directory>

Use <RequireAll> instead to demand both. More combinations are in the access control guide.

Digest authentication: skip it

AuthType Digest (mod_auth_digest) sends a hash instead of the password. The Apache documentation now says it no longer meets its goal: an attacker in the middle can force the browser down to Basic, the MD5 hash is fast enough to brute-force with graphics cards, and a stolen htdigest file can be used to log in directly. Its advice is Basic authentication over an encrypted connection. The module has also had a run of security fixes: httpd 2.4.67 to 2.4.69 fixed several mod_auth_digest vulnerabilities (timing, replay and denial-of-service issues), so if you keep it, stay on the current release. If an old client needs Digest, this works:

sudo htdigest -c /etc/apache2/.htdigest "Staff area" carol
sudo a2enmod auth_digest && sudo systemctl restart apache2   # Debian, Ubuntu

<Location "/staff/">
    AuthType Digest
    AuthName "Staff area"
    AuthDigestDomain "/staff/"
    AuthDigestProvider file
    AuthUserFile "/etc/apache2/.htdigest"
    Require valid-user
</Location>

The realm given to htdigest must match AuthName exactly.

LDAP and Active Directory

To let staff log in with their directory account, use mod_authnz_ldap, which relies on mod_ldap for connections and caching:

sudo a2enmod authnz_ldap && sudo systemctl restart apache2   # Debian, Ubuntu (enables ldap too)
sudo dnf install mod_ldap && sudo systemctl restart httpd     # RHEL, Fedora

An Active Directory example over LDAPS (port 636), allowing one group:

# Server config only: the CA that signed the domain controllers' certificates (PEM)
LDAPTrustedGlobalCert CA_BASE64 "/etc/ssl/certs/corp-ca.pem"

<Location "/intranet">
    AuthType Basic
    AuthName "Company login"
    AuthBasicProvider ldap
    AuthLDAPURL "ldaps://dc1.corp.example.com:636/dc=corp,dc=example,dc=com?sAMAccountName?sub?(objectClass=user)"
    AuthLDAPBindDN "CN=svc-apache,OU=Service Accounts,DC=corp,DC=example,DC=com"
    AuthLDAPBindPassword "exec:/bin/cat /etc/apache2/ldap-bind.secret"
    Require ldap-group CN=Web Editors,OU=Groups,DC=corp,DC=example,DC=com
</Location>

Checking that it works

$ curl -sI https://www.example.com/private/ | grep -i -e '^HTTP' -e authenticate
HTTP/2 401
www-authenticate: Basic realm="Private area"

$ curl -s -o /dev/null -w '%{http_code}\n' -u alice https://www.example.com/private/
Enter host password for user 'alice':
200

Failed logins appear in the error log (date and process fields trimmed):

[auth_basic:error] [client 203.0.113.9:51234] AH01617: user alice: authentication failure for "/private/": Password Mismatch
[auth_basic:error] [client 203.0.113.9:51240] AH01618: user nobody not found: /private/

Common mistakes

Sources