NGINX Server Blocks
NGINX Server Blocks
apt install nginx
apt install nginx
systemctl status nginx
(1:546)# systemctl status nginx.service
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-10-05 06:45:22 CDT; 4h 11min ago
Docs: man:nginx(8)
Process: 829 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Process: 886 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Main PID: 890 (nginx)
Tasks: 3 (limit: 2309)
Memory: 11.6M
CPU: 101ms
CGroup: /system.slice/nginx.service
├─890 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
├─891 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
└─892 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
Oct 05 06:45:21 vhost2023-jamesfraze systemd[1]: Starting A high performance web server and a reverse proxy server...
Oct 05 06:45:22 vhost2023-jamesfraze systemd[1]: Started A high performance web server and a reverse proxy server.
Purchase and Name Server Update
(Route 53 > Registered Domains > jamesfraze.com )[https://us-east-1.console.aws.amazon.com/route53/domains/home]
Route53 has DNSSEC and other features that digitalocean.com does not. So if I use DNSSEC, then I will need to pay the $6 a year and leave/manage with Route53. If I Do not want DNSSEC, then I’ll purchase through Route53, but then manage through digitalocean.
Delete Hosted Zone (Maybe)
I purchase from route53, but then delete the hosted zone and update name servers to digitalocean.com.
(Route 53 > Hosted Zones)[https://us-east-1.console.aws.amazon.com/route53/v2/hostedzones]
If you do not delete the hosted zone amazon charges you $6 a year to “manage” your hosted zone.
Verify A Records
If you haven’t already created proper A records and CNAME records, do that first as it could take up to 48 hours for A records to propagate. You will need this to be completely ready before you attempt to get an SSL cert from certbot.
In digitalocean: Networking > Domains > jamesfraze.com “Create New Record”
Create CNAME Records
For my particular case Here is the scenario I want:
sub.domain | desired action | TLS | Record | Value |
---|---|---|---|---|
jamesfraze.com | nothing, no DNS | ?? | ||
www.jamesfraze.com | ?? | ?? | ||
hosts.jamesfraze.com | show a default page | TLS | ||
grimoire.jamesfraze.com | show my content | TLS |
dig A hosts.jamesfraze.com
; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> A hosts.jamesfraze.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36494
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;hosts.jamesfraze.com. IN A
;; ANSWER SECTION:
hosts.jamesfraze.com. 0 IN A 127.0.1.1
;; Query time: 0 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Thu Oct 05 11:16:13 CDT 2023
;; MSG SIZE rcvd: 65
Use cloudflare or other DNS servers and tools like dnschecker to verify your A record from multiple DNS servers.
(1:1473)# dig A grimoire.jamesfraze.com @1.1.1.1
; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> A grimoire.jamesfraze.com @1.1.1.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29363
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;grimoire.jamesfraze.com. IN A
;; ANSWER SECTION:
grimoire.jamesfraze.com. 300 IN A 159.203.147.69
;; Query time: 156 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
;; WHEN: Sun Oct 08 07:50:54 CDT 2023
;; MSG SIZE rcvd: 68
Set Hostname
Only needs to be done on main/default host (not every server block)
hostnamectl set-hostname hosts.jamesfraze.com
Verify Host File
Also, only done on main server default, not every server block
cat /etc/hosts
127.0.1.1 hosts.jamesfraze.com
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
Create htpassword file
Only needs to be done once, unless you have multiple zones with the same user. Note that htpassword is not the same as htdigest. htpassword is considered less secure. htdigest uses a combination of realm and password to create a unique digest that is “hopefully” not in a rainbow table somewhere. Whereas htpassword has no salt, so is likely in a rainbow table somewhere. If your hash is revealed, it can be hacked easily.
We are creating the .htpasswd file, which will hold the hash and user name out of the web directory. An attacker would need to do directory traversal or somehow get this file that is not normally visible for it to be of any use.
Create a basic auth file as nginx cannot do htdigest:
htpasswd -c /etc/nginx/.htpasswd james
Add a Web User
If you have a user managing multiple hosts, it is still best practice to have a different webuser for each to avoid pivoting and getting data by abusing permissions.
adduser grimoire
usermod -aG sudo grimoire
This will create a folder for the user, but the folder is not web accessible.
(1:1477)# ls /home/ -alF
total 16
drwxr-xr-x 4 root root 4096 Oct 5 12:31 ./
drwxr-xr-x 20 root root 4096 Oct 8 07:00 ../
drwxr-x--- 3 grimoire grimoire 4096 Oct 5 12:45 grimoire/
Create Structure as User
We su as the user to properly set the file permissions. This is mostly useful in a multi-user environment. Later you will see I manually set user, group and permissions via a script. This is the best practice way though to creating files/folders as a different user to keep permissions correct:
su - grimoire
sudo mkdir -p /var/www/grimoire.jamesfraze.com/html
sudo chown -R $USER:$USER /var/www/grimoire.jamesfraze.com/html
sudo chmod -R 755 /var/www/grimoire.jamesfraze.com
vim /var/www/grimoire.jamesfraze.com/html/index.html
Main Site Config Example
The main site is different than the server blocks, which are considered virtual hosts.
(1:553)# cat /etc/nginx/nginx.conf
This is a good starting point for security, rate limiting, optimizations, etc. This is not a one sized fits all configuration though. It was customized for my 2 cpu, 2GB server that only serves static pages.
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
server_tokens off;
# Performance settings
tcp_nodelay on;
keepalive_timeout 30s;
server_names_hash_bucket_size 64;
# server_name_in_redirect off;
# MIME Types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# DOS Protections
#limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
#limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=300r/m;
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Compression Settings
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\.";
# Directory Listing, default is off
autoindex off;
# Buffer Protection
client_body_buffer_size 16K;
client_header_buffer_size 1k;
client_max_body_size 10m;
large_client_header_buffers 4 8k;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Server Block Example (Virtual Host)
The “default” server block or virtual host is located here:
cat /etc/nginx/sites-available/default
We are going to use it as a base to create a new server block/vhost:
vim /etc/nginx/sites-available/grimoire.jamesfraze.com.conf
Here is a final example after certbot. Some things will need to be commented out as they will not work (301 redirection to https for example, ssl_session_cache, STS/HTS, OSCP stapling, listen 443, etc. You can enable them after certbot to look like this, but for now modify the default http into your new hostname.conf file and do not add all of these items yet:
server {
if ($host = grimoire.jamesfraze.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
server_name grimoire.jamesfraze.com;
listen 80;
return 404; # managed by Certbot
}
server {
# Basic Configuration, redirected from 80
ssl_session_cache shared:SSL:10m;
server_name grimoire.jamesfraze.com;
root /var/www/grimoire.jamesfraze.com/html;
index index.html index.htm;
# Caching for static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf)$ {
expires 30d;
}
# Security Headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com 'unsafe-inline'; connect-src 'self' https://www.google-analytics.com" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Permissions-Policy "accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=self, gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=()";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# OSCP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.4.4 1.0.0.1 208.67.222.222 9.9.9.9 valid=300s;
resolver_timeout 5s;
# Deny access to dot files, disable for .well-known checks
location ~ /\.well-known {
allow all;
}
# But block the rest of the dot files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location / {
# Rate limiting, General
limit_req zone=mylimit burst=50 nodelay;
limit_conn conn_limit_per_ip 20;
# Authentication
auth_basic 'RESTRICTED';
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ =404;
# Allowed HTTP methods
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 405;
}
}
# Logging
access_log /var/log/nginx/grimoire.jamesfraze.com_access.log;
error_log /var/log/nginx/grimoire.jamesfraze.com_error.log;
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
listen 443 ssl http2; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/grimoire.jamesfraze.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/grimoire.jamesfraze.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
}
Enable a Site
Apacher uses a2ensite which basically creates a symlink. Nginx does not have this utility. Intead you create the symlink manually.
sudo ln -s /etc/nginx/sites-available/example.conf /etc/nginx/example
Disable a Site
If you do need to disable a site, this is the way:
sudo rm /etc/nginx/sites-enabled/example.conf
Basic Files
touch the basic files
(1:556)# pwd
/var/www/grimoire.jamesfraze.com
(1:556)# ls html/
total 12
drwxr-xr-x 2 grimoire www-data 4096 Sep 25 22:50 ./
drwxr-xr-x 3 root root 4096 Sep 25 20:21 ../
-rw-r--r-- 1 grimoire www-data 0 Sep 25 22:48 404.html
-rw-r--r-- 1 grimoire www-data 0 Sep 25 22:48 50x.html
-rw-r--r-- 1 grimoire www-data 0 Sep 25 22:50 favicon.ico
-rw-rw-r-- 1 grimoire www-data 123 Sep 25 22:47 index.html
Test Configuration
nginx -t
Restart NGINX
systemctl restart nginx
systemctl status nginx
Error/Access Logs
The main site access/error logs are separate from each vhost, if you configured the way I recommended:
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
Each vhost has it’s own logs:
tail -f /var/log/nginx/grimoire.jamesfraze.com_error.log
tail -f /var/log/nginx/grimoire.jamesfraze.com_access.log
Verify FW Access
If using UFW and host based firewall:
(1:562)# ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere # SSH for IPv4 only
[ 2] 80/tcp ALLOW IN Anywhere # HTTP for IPv4 only
[ 3] 443/tcp ALLOW IN Anywhere # HTTPS for IPv4 only
[ 4] 443/udp ALLOW IN Anywhere # QUIC/HTTP2 IPv4 only
If using digitalocean, Droplets > the droplet > Networking > Firewall Section (review the WAF)
Verify Browser Access
From a remote host, not the server, verify it works
curl http://grimoire.jamesfraze.com/
Next Steps
If it works, it’s time to install the cert using certbot.