Skip to content

Instantly share code, notes, and snippets.

@dominikwilkowski
Last active October 19, 2020 03:52
Show Gist options
  • Save dominikwilkowski/435054905c3c7abc2badc92a0acff4ba to your computer and use it in GitHub Desktop.
Save dominikwilkowski/435054905c3c7abc2badc92a0acff4ba to your computer and use it in GitHub Desktop.
Ubuntu setup with NGINX http/2 and letsencrypt

Intro

This is a basic collection of things I do when setting up a new headless ubuntu machine as a webserver. Following the steps below should give you a reasonable secure server with HTTP/2 support (including ALPN in chrome) and the fast NGINX server. I am happy to add things so leave a comment.

Basics

After creating the server (droplet on DigitalOcean) log in with

ssh root@[IP ADDRESS]

Once inside the machine set a password for root:

passwd

Update your package manager:

apt-get update
apt-get upgrade
apt autoremove
apt-get dist-upgrade

Install fail2ban:

apt-get install fail2ban

Install vim:

apt-get install vim vim-scripts vim-doc vim-latexsuite vim-gui-common vim-gnome vim-gtk

Create a new user:

useradd deploy
mkdir /home/deploy
mkdir /home/deploy/.ssh
chmod 700 /home/deploy/.ssh

Give the new user a sudo password:

passwd deploy

Add your ssh key:

vim /home/deploy/.ssh/authorized_keys         #add your public ssh key here, copy your public ssh key from your local with: `pbcopy < ~/.ssh/id_rsa.pub`, create a new one with `ssh-keygen -t rsa`
chmod 400 /home/deploy/.ssh/authorized_keys   #permissions
chown deploy:deploy /home/deploy -R           #owner

And add the user to the superuser group

visudo

Add into the file:

root    ALL=(ALL) ALL
deploy  ALL=(ALL) ALL

Disable root login and password authentication

vim /etc/ssh/sshd_config

Edit:

PermitRootLogin no
PasswordAuthentication no
AllowUsers deploy@(your-ip) deploy@(another-ip-if-any)    #you can even whitelist IPs from where you connect from (optional)

Restart the ssh service

service ssh restart

Now test your login with the new user in a new shell:

ssh deploy@[ID ADDRESS]

If everything works with the deploy login, log out of you root session and close.


NOW LOGIN WITH DEPLOY AND INSTALL EVERYTHING WITH DEPLOY

Install unattended-upgrades

sudo apt-get install unattended-upgrades

Edit what is updated:

sudo vim /etc/apt/apt.conf.d/10periodic

Modify so you have:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

and specify what upgrades should be done:

sudo vim /etc/apt/apt.conf.d/50unattended-upgrades

The default here is often fine:

Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-security";
};

Now install logwatch:
(This will typically install postfix and ask you about your mail setup)

sudo apt-get install logwatch

Now edit what to do with the logs. I usually have them send to my email to sort them into a folder there:

sudo vim /etc/cron.daily/00logwatch

Modify so you have something like (change [email protected] to your email):

/usr/sbin/logwatch --output mail --mailto [email protected] --detail high

I like to disable /run/shm read/write privileges as they are not needed in a headless server:

sudo vim /etc/fstab

And add the following line:

none            /run/shm        tmpfs   defaults,ro              0       0

Install rootkit detection with RKHunter and CHKRootKit:

sudo apt-get install rkhunter chkrootkit

Configure CHKRootKit:

sudo vim /etc/chkrootkit.conf

Modify so you have:

RUN_DAILY="true"

To run RKHunter execute the following command as often as you update apt-get:

sudo rkhunter --update

(This will update it’s database and performe a check)

bash

I like to change the shell to bash as that’s what I like.

sudo apt-get install csh
sudo chsh -s /bin/bash deploy

(see after reconnect)

firewall

First make sure you have IPv6 enabled:

sudo vim /etc/default/ufw

It should say:

IPV6=yes

Then make sure you are not locked out:

sudo ufw allow ssh

I would also usually do:

sudo ufw allow https
sudo ufw allow http
sudo ufw allow ftp

Then enable the firewall:

sudo ufw enable

And to make sure you have everything enabled and no useless ports open:

sudo ufw status

To reload the firewall you can:

sudo ufw reload

And to see the log you do:

sudo grep UFW /var/log/syslog

timezone

To set your correct timezone:

sudo dpkg-reconfigure tzdata

Install the time protocol daemon

sudo apt-get install ntp

swapfile

Now as we all have moved to SSD servers we don’t need this anymore. :)
For legacy reasons:

get memory with `free -m`
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'

FTP

sudo apt-get install vsftpd

Edit the config:

sudo vim /etc/vsftpd.conf

Modify so you have:

anonymous_enable=NO
write_enable=YES

And restart your service:

sudo service vsftpd restart

(connection via SFTP)

git

Install git if it isn’t already:

sudo apt-get install git

And set your account:

git config --global user.name "Your Name"
git config --global user.email "[email protected]"

node

Install node and NPM:

sudo apt-get install nodejs
sudo apt-get install npm

Because there was a name clash in the package manager we have to add a symlink to get the node namespace working:

sudo ln -s /usr/bin/nodejs /usr/bin/node

nginx

Install your server of choice. NGINX is what I like:

sudo apt-get install nginx

For HTTP/2 to be supported you will need to check the version of NGINX and openSSL:

nginx -v
openssl version

NGINX needs to be at least 1.9.5 or above and OpenSSL needs to be at least 1.0.2 or above (for ALPN to work in chrome).

Create a symlink to get to your html folder quickly and give permission:

sudo ln -s /var/www/html/ /www
sudo chown deploy:deploy /var/www/html/ -R

And edit the NGINX config:

sudo vim /etc/nginx/sites-available/default

To delete a page in vim just type :1,$d in the command prompt. I usually write the config locally and then just past it into vim. So: copy content, open file in vim, do :1,$d to delete contents, type i to set vim into insert mode, paste.

(I’ve attached my basic config that works for me below, note that this config assumes SSL and the cypher below)

Test your config with:

sudo nginx -t

To create a strong cypher:

sudo mkdir /etc/nginx/ssl/
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096

(This takes some time)

Restart the server with the new config:

sudo nginx -s reload

letsencrypt

Install letsencrypts certbot because we want to add SSL to our website?

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python-certbot-nginx 

Now let's run the certbot

sudo certbot --nginx certonly

Set up auto renewal. Open crontab:

sudo crontab -e

And add the two following lines:

SHELL=/bin/bash # I like bash

# add timestamp to your log file for easier parsing
29 2 * * 1 date >> /var/log/letsencrypt-renewal.log 2>&1

# runs every Monday at 2:30AM, output is saved to /var/log/letsencrypt-renewal.log
30 2 * * 1 certbot renew >> /var/log/letsencrypt-renewal.log 2>&1

# restart the server 2:50AM
50 2 * * 1 /etc/init.d/nginx restart >> /var/log/letsencrypt-renewal.log 2>&1
# empty line at the end so cron doesn’t ignore the last command

This will try to renew your certs every Monday night and restart NGINX while saving the log into /var/log/letsencrypt-renewal.log. (Check your log if you have issues with vim /var/log/letsencrypt-renewal.log)

Now test your SSL certs via:

# NGINX CONFIG WITH HTTP/2 SUPPORT, CORES, A PROXY TO A NODE APP, HTTP -> HTTPS and WWW -> HTTPS redirects
#
# Make sure you edit the sections marked with "CHANGE THIS" comments.
#
server_tokens off;
# prevent clickjacking attacks
add_header X-Frame-Options SAMEORIGIN;
# disallow circumventing declared MIME types
add_header X-Content-Type-Options nosniff;
# X-XSS-Protection
add_header X-XSS-Protection '1; mode=block';
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains;' always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://ssl.google-analytics.com https://cdn.polyfill.io; img-src 'self' https://ssl.google-analytics.com; style-src 'self'; font-src 'self'; frame-src 'self'; object-src 'none'";
# CORS
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# brotli
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# node proxy
#
server {
listen 8080;
server_name [YOUR IP]; #CHANGE THIS
access_log /var/log/nginx/nodeApp.log;
error_page 400 401 402 403 404 405 500 501 502 503 504 @error_page;
# fallback page when node app is off
#
location @error_page {
root /var/www/html/;
internal;
rewrite ^ [https://domain.tld/error.html]; #CHANGE THIS
break;
}
location / {
proxy_redirect off;
proxy_pass_header Server;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_connect_timeout 5;
proxy_read_timeout 240;
proxy_intercept_errors on;
proxy_pass http://127.0.0.1:1337; #the nodeApp is listening on port 1337 internally only (make sure port 1337 is denied by ufw)
}
}
# http to https redirect
#
server {
server_name www.[domain.tld] [domain.tld]; #CHANGE THIS
root /var/www/html/;
return 301 https://[domain.tld]$request_uri;
}
# www to https redirect
#
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.[domain.tld]; #CHANGE THIS
ssl on;
ssl_certificate /etc/letsencrypt/live/[domain.tld]/fullchain.pem; #CHANGE THIS
ssl_certificate_key /etc/letsencrypt/live/[domain.tld]/privkey.pem; #CHANGE THIS
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem; #CHANGE THIS
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; #generate here: https://mozilla.github.io/server-side-tls/ssl-config-generator/
ssl_stapling on;
ssl_stapling_verify on;
location ~ /\.ht {
deny all;
}
return 301 https://[domain.tld]$request_uri; #CHANGE THIS
}
# ssl and http2 config
#
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name [domain.tld]; #CHANGE THIS
root /var/www/html/;
ssl on;
ssl_certificate /etc/letsencrypt/live/[domain.tld]/fullchain.pem; #CHANGE THIS
ssl_certificate_key /etc/letsencrypt/live/[domain.tld]/privkey.pem; #CHANGE THIS
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem; #CHANGE THIS
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; #generate here: https://mozilla.github.io/server-side-tls/ssl-config-generator/
# OCSP Stapling ---
# fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;
# root server
#
location / {
root /var/www/html/; #CHANGE THIS
index index.html index.htm;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
location ~ /\.ht {
deny all;
}
}
@dominikwilkowski
Copy link
Author

PS on DDOS I find this article a great resource:
https://easyengine.io/tutorials/nginx/fail2ban/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment