Password Protection with .htaccess
Sometimes you need to restrict access to a page, a directory, or an entire site to only those who know the password. Whether it is a staging environment that clients should not stumble upon, an admin panel that needs an extra layer of defense, or a private section for internal team members, Apache's built-in HTTP authentication provides a straightforward solution without writing any application code.
This guide covers everything you need to set up password protection using .htaccess: creating and managing password files, configuring authentication directives, choosing between user-level and group-level access, and understanding the security limitations you should be aware of before relying on this mechanism in production.
htpasswd Files
The foundation of Apache's password protection is the htpasswd file, a plain text file that stores usernames and their hashed passwords. Apache reads this file every time someone attempts to access a protected resource and compares the credentials they provide against the entries in the file.
Creating an htpasswd File
The htpasswd command-line utility (included with every Apache installation) creates and manages password files. Here is how to use it.
Create a new file with the first user:
htpasswd -c /etc/apache2/.htpasswd admin
- The
-cflag creates a new file. If the file already exists, it is overwritten completely. /etc/apache2/.htpasswdis the path where the file will be stored.adminis the username.
After running this command, you are prompted to enter and confirm a password:
New password: ********
Re-type new password: ********
Adding password for user admin
Add additional users to an existing file:
htpasswd /etc/apache2/.htpasswd editor
htpasswd /etc/apache2/.htpasswd john
Notice there is no -c flag when adding to an existing file. This is critical.
The -c flag destroys the existing file and creates a new one. If you accidentally use -c when adding a second user, you will lose all previously created users. Only use -c when creating the file for the very first time.
Update an existing user's password:
htpasswd /etc/apache2/.htpasswd admin
If the username already exists in the file, htpasswd replaces the old password with the new one.
Delete a user:
htpasswd -D /etc/apache2/.htpasswd john
The -D flag removes the specified user from the file.
View the contents of the file:
cat /etc/apache2/.htpasswd
Output:
admin:$apr1$xyz12345$A1B2C3D4E5F6G7H8I9J0K1
editor:$apr1$abc67890$L2M3N4O5P6Q7R8S9T0U1V2
Each line contains a username and a hashed password separated by a colon. The password is never stored in plain text.
Storing It Outside the Web Root
The htpasswd file contains password hashes that, while not plain text, should never be accessible via a browser. If someone can download your htpasswd file, they can run offline brute-force attacks against the hashes at their leisure.
Wrong approach:
/var/www/html/ ← Document root (publicly accessible)
├── index.html
├── .htaccess
├── .htpasswd ← DANGER: Inside the web root!
└── admin/
└── index.php
If the file is inside your document root, a direct request to https://example.com/.htpasswd could potentially serve its contents. While Apache blocks .ht* files by default, misconfigurations, alternative web servers, or proxy setups could expose it.
Correct approach:
/etc/apache2/.htpasswd ← Outside the web root (not accessible via HTTP)
/var/www/html/ ← Document root
├── index.html
├── .htaccess
└── admin/
└── index.php
Store the htpasswd file in a location that is completely outside the document root. Common locations include:
/etc/apache2/.htpasswd/home/username/.htpasswd(on shared hosting)/var/www/.htpasswd(one level above the typical document root)
Your .htaccess file references the htpasswd file using its absolute filesystem path, so it works regardless of where the file is located.
If you are on shared hosting and cannot store files outside the web root, make sure Apache's default protection for .ht* files is in place. Add this to your .htaccess as a safety net:
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
This ensures the .htpasswd file returns 403 Forbidden even if someone requests it directly.
Password Hashing
The htpasswd utility supports several hashing algorithms. The algorithm determines how securely the passwords are stored and how resistant they are to brute-force attacks.
Default behavior (recommended):
# Uses the default algorithm (bcrypt on most modern systems, or APR1/MD5)
htpasswd /etc/apache2/.htpasswd username
Explicitly specify bcrypt (most secure):
htpasswd -B /etc/apache2/.htpasswd username
The -B flag forces bcrypt hashing, which is the strongest option available.
Specify bcrypt cost factor:
htpasswd -B -C 12 /etc/apache2/.htpasswd username
The -C flag sets the bcrypt cost factor (default is 5, range is 4-17). Higher values are slower to compute, making brute-force attacks more expensive. A value of 10-12 is a good balance between security and login speed.
Here is a comparison of the available algorithms:
| Flag | Algorithm | Hash Example Prefix | Security Level | Notes |
|---|---|---|---|---|
-B | bcrypt | $2y$ | Strongest | Recommended. Slow by design. |
| none | APR1 (MD5) | $apr1$ | Moderate | Default on many systems. Adequate. |
-d | crypt | No prefix | Weak | Legacy Unix crypt. Only 8-char passwords. |
-s | SHA-1 | {SHA} | Weak | No salt. Vulnerable to rainbow tables. |
-p | Plain text | No prefix | None | Never use. Passwords stored in clear. |
Wrong approach:
# Plain text storage - anyone who reads the file sees all passwords
htpasswd -p /etc/apache2/.htpasswd username
# SHA-1 without salt - vulnerable to precomputed attacks
htpasswd -s /etc/apache2/.htpasswd username
Correct approach:
# Bcrypt - secure, salted, computationally expensive to crack
htpasswd -B /etc/apache2/.htpasswd username
Some very old Apache installations (pre-2.4) may not support bcrypt. In those cases, APR1/MD5 (the default) is the best available option. If you are running Apache 2.4 or later, always use bcrypt with the -B flag.
AuthType Basic
With the htpasswd file created, the next step is configuring .htaccess to require authentication. The AuthType directive specifies which authentication scheme Apache should use.
AuthType Basic
Basic authentication works by having the browser prompt the user for a username and password. The browser then sends these credentials with every subsequent request to the protected area, encoded in Base64 in the Authorization header.
Here is a minimal, complete configuration:
AuthType Basic
AuthName "Restricted Area"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
When a user visits a page protected by this configuration, their browser displays a login dialog. After entering valid credentials, they gain access. The browser remembers the credentials for the session (until the browser is closed or the cache is cleared).
How Basic Auth Works Under the Hood
The exchange between browser and server follows this sequence:
- Client requests the protected resource.
- Server responds with
401 Unauthorizedand aWWW-Authenticate: Basic realm="Restricted Area"header. - Browser shows a login dialog to the user.
- User enters username and password.
- Browser sends the request again with an
Authorization: Basic dXNlcjpwYXNzheader (Base64-encodedusername:password). - Server decodes the header, looks up the username in the htpasswd file, hashes the provided password, and compares it to the stored hash.
- If they match, the server grants access and serves the resource. If not, step 2 repeats.
Base64 is encoding, not encryption. Anyone who intercepts the HTTP traffic can trivially decode the credentials. Basic authentication should always be used over HTTPS. Without HTTPS, credentials are transmitted in effectively plain text across the network.
AuthName and AuthUserFile
These two directives work alongside AuthType to complete the authentication configuration.
AuthName
AuthName sets the realm displayed in the browser's login dialog. It tells the user what they are logging into.
AuthName "Restricted Area"
The browser typically displays this as:
The server https://example.com requires a username and password.
The server says: Restricted Area
Username: [________]
Password: [________]
[Cancel] [Log In]
The realm name also has a functional purpose: browsers group credentials by realm. If two different directories use the same AuthName, the browser automatically sends the same credentials to both without prompting again. If they use different realm names, the browser treats them as separate authentication scopes.
AuthName "Admin Panel"
AuthName "Reports Section"
With different realm names, users must authenticate separately for each section, even if their credentials are valid for both.
AuthUserFile
AuthUserFile specifies the absolute filesystem path to the htpasswd file containing the usernames and password hashes.
AuthUserFile /etc/apache2/.htpasswd
This must be an absolute path, not a relative one. Apache needs to know exactly where on the filesystem to find the file.
Wrong approach:
# Relative paths do not work reliably
AuthUserFile .htpasswd
AuthUserFile ../secrets/.htpasswd
Correct approach:
# Always use absolute paths
AuthUserFile /etc/apache2/.htpasswd
AuthUserFile /home/username/.htpasswd
If you are on shared hosting and unsure of the absolute path, you can find it by creating a PHP file that outputs the server's document root:
<?php
echo $_SERVER['DOCUMENT_ROOT'];
// Output example: /home/username/public_html
If the document root is /home/username/public_html, you could store your htpasswd file at /home/username/.htpasswd (one level above the web root).
Complete Configuration Examples
Protect an entire directory:
AuthType Basic
AuthName "Admin Panel"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
Protect a single file:
<Files "secret-report.pdf">
AuthType Basic
AuthName "Confidential Document"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
</Files>
Protect specific file types:
<FilesMatch "\.(pdf|xlsx|docx)$">
AuthType Basic
AuthName "Document Library"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
</FilesMatch>
Require valid-user vs Require user
The Require directive determines which authenticated users are granted access. There are two main approaches.
Require valid-user
Require valid-user
This grants access to any user listed in the htpasswd file, as long as they provide the correct password. If the htpasswd file contains 50 users, all 50 can access the protected resource.
This is the most common choice when everyone in the password file should have the same level of access.
Require user
Require user admin editor
This grants access only to the specifically named users, even if other valid users exist in the htpasswd file. A user named john could have a valid entry in the htpasswd file, but if he is not listed in the Require user directive, he is denied access with a 401 response.
This is useful when different directories should be accessible to different people:
AuthType Basic
AuthName "Admin Panel"
AuthUserFile /etc/apache2/.htpasswd
Require user admin superadmin
AuthType Basic
AuthName "Reports"
AuthUserFile /etc/apache2/.htpasswd
Require user admin editor analyst
In this setup, the admin user can access both areas, editor and analyst can only access reports, and superadmin can only access the admin panel.
Combining Require Directives
You can combine multiple Require directives with authorization containers for more complex rules:
Allow specific users OR specific IPs:
AuthType Basic
AuthName "Mixed Access"
AuthUserFile /etc/apache2/.htpasswd
<RequireAny>
# Office IP can access without a password
Require ip 203.0.113.0/24
# Remote users need credentials
Require user admin editor
</RequireAny>
Clients from the office network get in without a login prompt. Everyone else must authenticate as either admin or editor.
Require authentication AND a specific IP:
AuthType Basic
AuthName "High Security"
AuthUserFile /etc/apache2/.htpasswd
<RequireAll>
Require user admin
Require ip 203.0.113.0/24
</RequireAll>
The user must be admin AND must be connecting from the specified IP range. Both conditions must be true.
Common Mistake: Forgetting AuthType or AuthUserFile
Wrong approach:
# Missing AuthType and AuthUserFile
AuthName "Protected"
Require valid-user
Apache will return a 500 Internal Server Error because it does not know what authentication method to use or where to find the password file. All four directives (AuthType, AuthName, AuthUserFile, Require) must be present together.
Correct approach:
AuthType Basic
AuthName "Protected"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
Limitations of Basic Auth
HTTP Basic authentication is simple and effective for many scenarios, but it has significant limitations you should understand before relying on it.
No Encryption Without HTTPS
Basic authentication transmits credentials encoded in Base64, which is trivially reversible. Without HTTPS, anyone on the same network (coffee shop Wi-Fi, corporate network, ISP) can intercept and read the username and password.
# What the Authorization header looks like:
Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM=
# Base64 decoded:
admin:password123
Always use Basic auth over HTTPS. If your site does not have HTTPS configured, set it up before enabling password protection. A redirect to HTTPS should come before any authentication:
# Force HTTPS first
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# Then require authentication
AuthType Basic
AuthName "Secure Area"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
No Logout Mechanism
Basic authentication has no built-in logout. Once a browser has accepted credentials, it continues sending them with every request until the browser is closed (and sometimes even after, due to credential caching). There is no "logout" button, no session expiration, and no way for the server to tell the browser to forget the credentials.
This makes Basic auth unsuitable for applications where users need to log out, switch accounts, or have their sessions expire after a period of inactivity.
No Brute-Force Protection
Apache's Basic auth has no built-in rate limiting or account lockout mechanism. An attacker can try thousands of username and password combinations per second without being blocked. To mitigate this, consider:
- Using strong, long passwords.
- Combining Basic auth with IP restrictions.
- Using
fail2banor similar tools at the server level to block repeated failed attempts.
AuthType Basic
AuthName "Double Protection"
AuthUserFile /etc/apache2/.htpasswd
<RequireAll>
Require valid-user
<RequireAny>
Require ip 203.0.113.0/24
Require ip 198.51.100.0/24
</RequireAny>
</RequireAll>
No Custom Login Page
The login prompt is a browser-native dialog box. You cannot style it, add your logo, include a "forgot password" link, or customize it in any way. It looks different in every browser and provides a utilitarian, often confusing experience for non-technical users.
If you need a branded login experience, you should implement form-based authentication in your application instead of relying on Basic auth.
Flat File Scalability
The htpasswd file is read and parsed on every authenticated request. With a handful of users, this is negligible. With thousands of users, it becomes a performance problem. Basic auth with htpasswd files is designed for small-scale use (dozens of users at most).
For larger user bases, consider:
mod_authn_dbdfor database-backed authentication.- Application-level authentication with proper session management.
- An identity provider with SSO (Single Sign-On).
When Basic Auth Is Appropriate
Despite its limitations, Basic auth is a perfectly good choice for:
| Scenario | Why It Works |
|---|---|
| Staging/development site protection | Quick to set up, keeps casual visitors out |
| Admin panel extra layer | Defense in depth on top of application auth |
| Small team internal tools | Simple user management, no infrastructure needed |
| Temporary access during maintenance | Easy to add and remove |
| CI/CD webhook protection | Machine-to-machine auth where UX does not matter |
When to Use Something Else
| Scenario | Better Alternative |
|---|---|
| Public-facing user login | Application-level form authentication |
| Hundreds or thousands of users | Database-backed auth or identity provider |
| Need for session management or logout | Application sessions with cookies |
| Compliance requirements (HIPAA, PCI, SOC2) | Enterprise identity solutions with MFA |
| Social login (Google, GitHub, etc.) | OAuth2/OpenID Connect |
Basic authentication is a tool, not a solution. It excels at quickly locking down resources during development and adding a lightweight extra barrier in front of sensitive areas. But for user-facing authentication where security, usability, and scalability matter, invest in a proper authentication system at the application level. The key is knowing when each approach is appropriate and never treating Basic auth as a substitute for real application security.