Introduction
In this tutorial you will learn how to restrict access to your website by editing the server and location blocks in the nginx configuration file. It is explained how to block selected client IP addresses and IP addresses from specific geographical locations (geo-based restriction), and how to restrict access by setting up a pair of username and password (HTTP Basic Authentication). This can be useful if you want to add additional protection for your web server. You will also learn how to restrict access to one web page only (e.g. https://example.com/admin/), and leave access to all other web pages on your website (e.g. https://example.com/) unrestricted.
Prerequisites
- Configured Ubuntu server and a user with sudo privileges (you can follow this tutorial).
- Installed nginx (you can follow this tutorial).
- Basic understanding of nginx and its configuration. You can follow the official tutorial.
- For the password-based restriction, you should enable HTTPS for your
server. You can follow this tutorial.
Example terminology
- Server IPv4: 10.0.0.1
- Local IPv4: 10.0.0.5
- Domain: example.com
Please replace 10.0.0.5 with an IP of your local computer, 10.0.0.1 with an IP of your server, and example.com with your own domain in all example commands.
Formatting conventions
-
In some configuration examples, the ellipsis
...is used to indicate that part of the configuration is omitted. -
server vs.
server-
server is your Ubuntu server on which nginx is installed.
-
server, on the other hand, is the block in the nginx configuration file which looks like this:server { ... }
-
Configuration places
You need to know where nginx stores its configuration.
-
/etc/nginxis the nginx configuration directory. All configuration files are placed there. This directory is used for the relative paths in theincludedirective. -
/etc/nginx/nginx.confis the main nginx configuration file. nginx starts reading its configuration here. -
/etc/nginx/sites-enabledis the directory where you can place your HTTP(S)serverconfiguration files. After nginx is started or reloaded, all files from there will be loaded by nginx. This works because the main file/etc/nginx/nginx.confcontains the directiveinclude sites-enabled/*;. Files are loaded in alphabetical order. -
/etc/nginx/sites-availableis the directory where you can place your HTTP(S)serverconfiguration files, but they will not be loaded by nginx. A configuration file in this directory can be activated by creating a symbolic link in the/etc/nginx/sites-enableddirectory. For example:ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/enables the configuration file
example.com. After the next startup or reload, nginx will load this file.
Log files
Log files are useful when you need to troubleshoot something. There are two log files by default:
-
/var/log/nginx/access.logis the file where nginx logs all requests. The format of this log file is calledcombinedand is explained here. -
/var/log/nginx/error.logis the file where nginx logs all error conditions and status information.
Error on reload
If you try to reload your configuration with the command sudo systemctl reload nginx and nginx
complains that you made a mistake, you can see the precise error by running sudo nginx -t.
Step 1 - Locating the server block file
You need to find the file that includes the server block for your domain. You can use ls -al /etc/nginx/sites-available/ to list the server block files that are saved on your server. Let's say you want to add restrictions for a server block that responds to:
curl http://example.com
example.comis your domain or the IP of your server
You need to find the server block that includes the line server_name example.com;.
In our example, the server block we are looking for is saved in the file /etc/nginx/sites-available/example.com and it looks like this:
server {
...
server_name example.com;
...
}- For HTTP, the
listendirective should belisten 80;orlisten 10.0.0.1:80;(10.0.0.1should be the IP of your server). - For HTTPS, the
listendirective should belisten 443 ssl;orlisten 10.0.0.1:443 ssl;.
For the curl example above, the configuration should be similar to this:
server {
listen 80;
server_name example.com;
...
}If you can't find a server block with the appropriate server_name line, it means that the server block in the default file (/etc/nginx/sites-available/default) is used. In this case, you need to find the server block with a listen directive that corresponds to the request port that appears first in the nginx configuration, or a listen directive that is marked as default, like this: listen 80 default_server;.
After the configuration of the server is found, you can start adding restrictions to your server.
Step 2 - Using IP addresses of clients to allow or deny access
nginx provides allow and deny directives to allow and block access for clients by using their IP addresses.
For testing purposes, you can block access to yourself.
Before doing so, you need to first check that you can access your server by running this command on your local computer:
curl -4 --head http://example.com
Replace
example.comwithserver_nameof theserveryou want to configure, andhttpwithhttpsif necessary.The combination of the domain (or an IP) and port must correspond to the nginx
server_nameandlistendirective (in this example, port80is used because it is the default for HTTP).
You should get the output similar to this, with the HTTP status 200:
HTTP/1.1 200 OK
Server: nginx
Date: Fri, 10 Mar 2023 13:22:45 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-aliveStep 2.1 - Finding out your public IP address
You need to know the public IP address of your local computer to block it in nginx. You can run this command to determine it:
curl -4 https://ip.hetzner.comYour IP address will be outputted to the terminal, for example: 10.0.0.5.
-
If the curl command above doesn't work, try this Google search query. You should see your public IP right in the search results.
-
If nginx is only listening on a local port, you need to use your local IP instead of the public one.
Step 2.2 - Blocking access
You need to add this line to the appropriate location context in your server block:
deny 10.0.0.5;Replace
10.0.0.5with your own public IP.
The complete configuration may look like this:
server {
listen 80;
server_name example.com;
location / {
deny 10.0.0.5;
}
}To apply your changes, reload the nginx configuration:
sudo systemctl reload nginxNow, let's try to run the same command from the introduction of this step on your local computer:
curl -4 --head http://example.comThis time, the output must be like this:
HTTP/1.1 403 Forbidden
Server: nginx
Date: Tue, 21 Mar 2023 08:27:58 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-aliveHTTP status
403means that access is denied.
In the nginx error log /var/log/nginx/error.log you should see an error like this:
2023/03/20 17:59:25 [error] 25216#1896: *18 access forbidden by rule, client: 10.0.0.5, server: example.com, request: "GET / HTTP/1.1", host: "10.0.0.1"
10.0.0.5is the IP of the blocked client, andexample.comis the value of theserver_namedirective.
Congratulations! You blocked access to yourself. To rollback your changes, you need to remove the deny directive and reload the nginx configuration.
Multiple allow and deny directives can be specified. The first directive that matches will be used.
The two special directives allow all; and deny all; always match, and they should be placed at the bottom of the allow / deny list.
For example:
allow 10.0.0.5;
allow 10.0.0.6;
deny all;Allow access to
10.0.0.5and10.0.0.6, and block everybody else.
The complete configuration:
server {
listen 80;
server_name example.com;
location / {
allow 10.0.0.5;
allow 10.0.0.6;
deny all;
}
}If your server listens on IPv6 ports, make sure to allow or deny IPv6 addresses too.
Instead of using the location context, you can also place the list of deny and allow directives in the server context:
server {
listen 80;
server_name example.com;
allow 10.0.0.5;
allow 10.0.0.6;
deny all;
location / {
}
}In this case, the child context location will inherit the list of allow and deny directives from its parent context server.
However, if you specify at least one deny or allow directive in the location context, the entire list of allow and deny directives from the parent context server will be ignored. For example:
server {
listen 80;
server_name example.com;
allow 10.0.0.5;
allow 10.0.0.6;
deny all;
location / {
deny 10.0.0.7;
}
}In the location context, only 10.0.0.7 is blocked, all other IPs are allowed.
The list of allow and deny directives from the parent context server is ignored.
Instead of IP addresses, you can also specify subnets. For example:
allow 10.0.0.0/24;
deny all;Do not forget to reload the configuration after you made your modifications. Simply run:
sudo systemctl reload nginxAttention!
-
If you have a configuration like this, requests to
example.com/admin/index.phpwill not be protected:The request is handled with the
location ~ \.php$context. This means that the allow and deny directives oflocation /adminare not used.server { listen 80; server_name example.com; location / { } location /admin { allow 10.0.0.5; allow 10.0.0.6; deny all; } location ~ \.php$ { try_files $fastcgi_script_name =404; fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi.conf; } }location /andlocation /adminare "prefix locations"location ~ \.php$is a "regular expression location"
In the example request
example.com/admin/index.php:- The path
/adminmatches with the "prefix location"location /admin. - The file
index.phpmatches with the "regular expression location"location ~ \.php$.
Nginx remembers the longest prefix location that matches the request, in this example that's
location/admin. However, it will only use the prefix location to process the request if no regular expression location matches the request. In this example, the regular expression locationlocation ~ \.php$matches the request and is used.Regular expression locations are marked with a tilde
~or~*. Starting from the top of theserverblock, nginx first checks every prefix location inside out. The first matching regular expression location that nginx can find is automatically used.Hence, the regular expression
locationmust be defined separately for the protected prefixlocationas show below. -
If you edit the configuration from above like this, requests to
example.com/admin/index.phpwill be protected:The request is handled with the first regular expression locations that matches the request. In this configuration, that's the
location ~ \.php$context within thelocation /admincontext. This means that the allow and deny directives oflocation /adminare used.server { listen 80; server_name example.com; location / { } location /admin { allow 10.0.0.5; allow 10.0.0.6; deny all; location ~ \.php$ { try_files $fastcgi_script_name =404; fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi.conf; } } location ~ \.php$ { try_files $fastcgi_script_name =404; fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi.conf; } } -
If you don't like duplication, you can put the common parts into a file and refer to this file via an
includedirective. In this example, the file/etc/nginx/php.confcan be created with these directives:try_files $fastcgi_script_name =404; fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi.conf;Now, it can be included:
server { listen 80; server_name example.com; location / { } location /admin { allow 10.0.0.5; allow 10.0.0.6; deny all; location ~ \.php$ { include php.conf; } } location ~ \.php$ { include php.conf; } }
Step 3 - Using the geo module to allow or deny access
As a more flexible alternative to the allow and deny directives, you can use the nginx module geo.
The geo module lets you create a variable with a value that depends on a client IP address. This variable can be used later in request processing.
This mechanism is not specifically for restricting access but can be used as such. Let's return to our example from the previous chapter which uses allow and deny, and which allows two IP addresses and blocks all others.
server {
listen 80;
server_name example.com;
location / {
allow 10.0.0.5;
allow 10.0.0.6;
deny all;
}
}This example can be translated to use the geo module:
0 = allowand1 = deny
geo $not_allowed {
10.0.0.5 0;
10.0.0.6 0;
default 1;
}
server {
listen 80;
server_name example.com;
location / {
if ($not_allowed) {
return 403 "You're not allowed to access. Your IP is $remote_addr";
}
}
}This approach is a little more flexible as you can see. You can set any HTTP status code by changing 403 to somethings else,
and you can show the message to the blocked client. The message can contain variables.
-
In contrast to the
denydirective, you will not see anything inerror.logwhen the user is blocked. As with other requests, it will be logged toaccess.log. -
If you want to use
return, then you need to usegeowithifinstead ofallowanddeny.allowanddenywill not work withreturn, becausereturnis executed earlier thanallowanddeny, and the result will be returned regardless if a user is allowed to access.
Step 4 - Configuring restrictions based on the geographical location
Requirements:
-
Ubuntu 20.04 or later.
-
nginx is installed from the Ubuntu repositories.
If you installed nginx from third-party repositories provided by nginx project, this instruction will not work because their repositories don't provide the required dynamic module.
First of all, you need to install the nginx dynamic module geoip2 which adds support for the IP databases with geographical information. The database will be queried to determine the country of the user by their IP. This information in turn will be used to allow or block access.
-
Install the required package:
sudo apt update && sudo apt install libnginx-mod-http-geoip2 -
Check the
/etc/nginx/nginx.conffile:By default, at the beginning of the file
/etc/nginx/nginx.confand outside of any curly brackets, you must have the uncommented line:include /etc/nginx/modules-enabled/*.conf;If it's not the case, you need to add or uncomment it.
Alternatively, you can load only
geoip2module like this:load_module modules/ngx_http_geoip2_module.so; -
Reload the nginx configuration:
sudo systemctl reload nginx
Now you only have the geoip2 module, this is not enough to start enabling restrictions.
You need to download the geoip2 compatible IP database that can be used to check the country of a user.
-
Install the required package:
cd /etc/nginx && sudo curl --fail -LO https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-Country.mmdbAlternatively you can create an account on the MaxMind website and download the GeoLite2 Country database from there:
- Open the registration form in your browser and fill it in.
- You will receive an email with the link to create your password.
- Log in with your email and password.
- Go to
Account / Manage License Keysand create a new license key. Copy your license key and save it somewhere. - Now you can download the GeoLite2 Country database by running this command:
cd /etc/nginx && sudo curl --fail -o GeoLite2-Country.tar.gz "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz" && sudo tar --strip-components=1 -xzf GeoLite2-Country.tar.gz --wildcards "*.mmdb" && sudo rm GeoLite2-Country.tar.gzReplace
YOUR_LICENSE_KEYwith your license key. The database is saved to the file/etc/nginx/GeoLite2-Country.mmdb. -
Edit the nginx configuration
Now you need to edit your nginx configuration to add the
geoip2directive which loads the database into nginx. After the database is loaded, it can be used to block or allow access. The complete configuration may look like this:0 = allowand1 = denygeoip2 /etc/nginx/GeoLite2-Country.mmdb { $user_country country iso_code; } map $user_country $not_allowed { CA 1; US 1; default 0; } server { listen 80; server_name example.com; location / { if ($not_allowed) { return 403 "You're not allowed to access."; } } }geoip2directive is used to specify the path to an IP database and to define variables.
In the example above, the variable
$user_countryis defined. When the request is processed by nginx, this variable is set to the user country's two-letter code.mapdirective is used to create the boolean variable$not_allowed. Its value is chosen according to the$user_countryvariable defined in thegeoip2block.
In this example configuration, the users from United States and Canada are blocked, users from other countries are allowed. The two-letter country codes which can be used inside the
mapare listed on this Wikipedia page.- Finally, the
$not_allowedvariable is used inside theifstatement to block or allow a request.
You can find more information about the geoip2 module on its GitHub page.
Step 5 - Password-based authentication
You can restrict access by only allowing access to users with a password. This is called HTTP Basic authentication. To prevent sending your username and password in clear text, you first need to setup HTTPS for your server.
You can use this tutorial to setup HTTPS. The configuration you will end up with will be similar to this:
server { server_name example.com; location / { } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot }After you have configured HTTPS, you can setup the password-based authentication.
First of all, you need to create a special file where all usernames and corresponding passwords are stored. You can create it like this:
sudo touch /etc/nginx/usersYou can use this command to add users:
-
Follow the prompts. If you made a mistake, you can cancel the command by pressing
Ctrl + C. -
Note that this command uses
sudoand may ask for your password, this is not the password for the new user.
while true; do
read -r -p "Username: " ngxuser
if [[ -z "$ngxuser" ]]; then
echo "Username is empty. Try again!"
continue
fi
ngxpass="$(openssl passwd -6)"
if (( $? != 0 )); then
echo "Try again!"
else
break
fi
done; echo "$ngxuser:$ngxpass" | sudo tee -a /etc/nginx/usersAbove command will add the user to the /etc/nginx/users file, and additionally output the username and hash of the password to your terminal separated by a colon :.
The password itself is not stored on the server.
You can add more users to /etc/nginx/users by repeating the same command above.
You can manually edit /etc/nginx/users to remove users. Each user is on a separate line.
The special file with users is created and you can now modify your server block.
You need to add the two directives auth_basic and auth_basic_user_file to your server configuration.
The resulting configuration will look like this:
server {
server_name example.com;
location / {
}
auth_basic "Protected area!";
auth_basic_user_file /etc/nginx/users;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}Reload nginx to apply your changes:
sudo systemctl reload nginxYou can test the new configuration with curl:
-
Without username and password:
curl --verbose https://example.comIn this case you should get HTTP status
401and no requested content. Response headers will look like this:< HTTP/1.1 401 Unauthorized < Server: nginx < Date: Fri, 24 Mar 2023 09:13:54 GMT < Content-Type: text/html < Content-Length: 179 < Connection: keep-alive < WWW-Authenticate: Basic realm="Protected area!" -
With username and password:
Replace
userwith your username,passwordwith your password created using the command above, andexample.comwith your domain.curl --verbose -u user:password https://example.comThis time you should get HTTP status
200and the requested content. Response headers will look like this:< HTTP/1.1 200 OK < Server: nginx < Date: Fri, 24 Mar 2023 09:06:12 GMT < Content-Type: text/html < Content-Length: 615 < Last-Modified: Mon, 20 Mar 2023 14:48:42 GMT < Connection: keep-alive < ETag: "6418724a-267" < Accept-Ranges: bytes
Restricting access to a single URL
-
You can restrict access to a single URL by adding the
auth_basicandauth_basic_user_filedirectives directly to alocationcontext instead of its parent-contextserver. For example:server { server_name example.com; location / { } location /admin { auth_basic "Protected area!"; auth_basic_user_file /etc/nginx/users; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot }Here,
location /adminis protected by password-based authentication.
Logging of wrong access details
-
If someone used the wrong password, it will be logged to the
/var/log/nginx/error.logfile like this:2023/03/28 12:06:49 [error] 1865#1865: *14 user "holu": password mismatch, client: 10.0.0.5, server: example.com, request: "GET / HTTP/1.1", host: "example.com" -
If someone used a username that doesn't exist in the
/etc/nginx/usersfile, it will be logged to theerror.logfile like this:2023/03/28 12:08:21 [error] 1865#1865: *16 user "root" was not found in "/etc/nginx/users", client: 10.0.0.5, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Conclusion
Hopefully this tutorial was useful to you and you learned something new. nginx can be tricky to configure, the official documentation and mailing list archives are useful resources to learn more. Mailing list archives can be searched by this Google search query.