Password-Protect a Directory with .htpasswd
htpasswd -c -B, AuthUserFile outside the web root, Require valid-user — and the RequireAny/RequireAll semantics that make IP rules actually work.
Two different jobs get conflated under “restrict access”: authentication (prove who you are — a password) and authorization by origin (prove where you’re from — an IP). Apache 2.4 does both in .htaccess; the trap is that the syntax looks simple enough to write wrong.
1. Make the password file — outside the web root
htpasswd -c -B /home/you/.htpasswd alice # -c creates the file, -B forces bcrypt
htpasswd -B /home/you/.htpasswd bob # second user: NO -c, or you wipe alice
Three things people get wrong here, in order of how badly they hurt:
- Location.
AuthUserFiletakes an absolute filesystem path —/home/you/.htpasswd, nothttps://…and not a relative name (relative resolves againstServerRoot, not your site). If the file sits insidepublic_html/htdocs, anyone can download it and crack your hashes offline. Outside the web root, always. - The
-cflag is one-time.htpasswd -ccreates a fresh file — running it again for a second user silently deletes the first. Additional users:htpasswdwith no-c. - Algorithm. Default MD5 is legacy;
-Bgives bcrypt (add-C 12for a stronger cost factor). Apache 2.4 verifies bcrypt fine.
chmod 640 the file and let it be readable by the Apache user.
2. The auth block
<IfModule mod_authn_file.c>
AuthType Basic
AuthName "Staging — team only"
AuthUserFile /home/you/.htpasswd
Require valid-user
</IfModule>
AuthType Basic— the browser’s native password prompt.Basicmeans Base64-encoded credentials on every request: pointless without HTTPS, since Base64 is encoding, not encryption. Force HTTPS below or beside this block, never instead of it.AuthName— the realm string shown in the prompt and used by password managers. Quotes needed if it has spaces.Require valid-user— any account in the file gets in. Alternatives:Require user alicefor one named account.- The
<IfModule mod_authn_file.c>guard prevents a 500 on hosts that load auth differently — at the cost of the protection silently not engaging if the module’s absent, so test it: hit the URL in a private window and expect a prompt.
3. IP rules — where not bites
Apache 2.4 replaced 2.2’s Order/Allow/Deny with Require + grouping containers. The semantics are the part that matters:
Allow-list — “only these may enter”:
<RequireAny>
Require ip 203.0.113.10
Require ip 198.51.100.0/24
Require host backup.example.com
</RequireAny>
RequireAny is OR — first matching line grants; non-matching requests get 403. Multiple Require ip lines must live in RequireAny: inside RequireAll they would AND, demanding the visitor be two IPs at once.
Deny-list — “everyone except these”:
<RequireAll>
Require all granted
Require not ip 203.0.113.66
</RequireAll>
This is the construct people write wrong. Require not ip X can only restrict — a not requirement never grants access to anyone. Written alone (or worse, OR’d with Require all granted — which lets everyone in before the not is even consulted), it either denies the entire site or does nothing. Inside RequireAll, all granted opens the door and each Require not ip slams it on the listed addresses. That is the only correct shape.
Both forms accept single IPs, CIDR ranges, partial IPv4 (10.1), IPv6 (2001:db8::/32), and hostnames (Require host — note the different directive, and that it costs a reverse-DNS lookup per request).
4. Combining auth and IP — the two real-world shapes
Belt and suspenders (staging): password required and from the office:
<RequireAll>
Require valid-user
Require ip 203.0.113.0/24
</RequireAll>
Office bypass (intranet): in-office IPs get in free, everyone else gets the login prompt:
<RequireAny>
Require ip 203.0.113.0/24
Require valid-user
</RequireAny>
Same directives, different container, opposite meaning — RequireAll is AND, RequireAny is OR. Pick deliberately.
5. Protecting one file instead of a directory
Wrap the auth in a <FilesMatch> container — the classic use is WordPress’s login endpoints:
<FilesMatch "^(wp-login|xmlrpc)\.php$">
AuthType Basic
AuthName "Login area"
AuthUserFile /home/you/.htpasswd
Require valid-user
</FilesMatch>
Troubleshooting the usual failures
- Prompt never appears → the
<IfModule>skipped (auth modules missing),AllowOverridelacksAuthConfig, or the file is in the wrong directory. - Prompt appears but no password works →
AuthUserFilepath wrong (check the error log: “could not open password file”), or-cwiped your user. - 403 for everyone including you → you wrote
Require not ipalone, or your allow-list is inRequireAlland your real IP isn’t in it. Check your IP withcurl ifconfig.me— office NATs surprise people.
The generator’s Password-protect and IP allow/deny sections emit all of the above with the comments inline — build your file. If the upload 500s, the debugging checklist applies.