Instructions for Fedora Server 34
As simple as that
# dnf install nginx
Considering www.example.com as our domain
First I added a simple server block with your correct domain and port 80 only. I don't know if it's really needed, I have to check
server {
server_name example.com;
root /usr/share/nginx/html;
listen 80;
}
# dnf install python-certbot-nginx
# certbot --nginx -d www.example.com
Certbot takes that server block and redirects it as https only. It redirects 80 to 443 automatically
After running certbot, you'll have a http block 80 redirected to 443 to secure your traffic.
We need to add this part to the secure server block. This tries to retrieve a static file. If it fails, it retrieves an answer from the Django server. We're also passing useful request headers to the django server.
location / {
try_files $uri @django_proxy;
}
location @django_proxy {
proxy_pass http://localhost:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
We don't want people to flood our server. This is especially true for dynamic routes. While static files are easy to serve, are usually cached or can be even outsourced to a CDN, this is not true for dynamic routes.
Each call to a dynamic route could make a SQL query, template generation wasting our energies. Maybe someone is trying to DDOS us or bruteforce some password. If the login page is not rate limited and the used password is very simple, this becomes a concrete risk.
Let's this line at the top of your nginx django.conf file. This creates a rule to limit each user to 5 requests per second. We'll use it later:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
Then, inside the proxy pass block, use the just defined my_limit rule.
location @django_proxy {
limit_req zone=mylimit burst=20;# <- added this line
proxy_pass http://localhost:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
You can spot I've added a burst queue of size 20. This allows the user to make burst requests (happens a lot when loading a page and having to retrieve many data by javascript), while avoiding flooding in the long run.
More information on rate limiting here.
This is my final /etc/nginx/conf.d/django.conf file. (Check if the conf.d folder is correct or there's a better one)
server {
server_name example.com;
root /usr/share/nginx/html;
location / {
try_files $uri @django_proxy;
}
location @django_proxy {
limit_req zone=mylimit burst=20;# <- added this line
proxy_pass http://localhost:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
error_page 404 /404.html;
listen [::]:443 ssl ipv6only=on; # managed by Certbot
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
}
server {
if ($host = example.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name example.com;
return 404; # managed by Certbot
}
The django software is run by an internal server and using a lower privileged user. This exchange data with nginx.
Create a django user by typing
# useradd -m django
Set its password if you want to login there, useful for acting in its home directory files
# passwd django
If you're root, you can also login to this user with
# su -- django
You need pip to proceed:
# dnf install python3-pip
To not put shit into the global python installation, it's recommended to place a self-contained python interpeter in a folder. Local dependencies will be placed there too.
Make sure virtualenv creator is installed in your system. You can do it in two ways:
# dnf install python3-virtualenv
# pip install virtualenv
In Debian, it is $ sudo apt install python3-venv
Login as django user, go in its home folder.
Create the virtualenv in a folder called venv or how you like:
$ python3 -m venv venv
Activate it with:
$ source /venv/bin/activate
From this point, all python commands will execute the virtualenv's interpreter and modules instead of the systemwide one. If you run pip install while in the virtualenv, dependencies will be placed inside it.
Deactivate with:
$ deactivate
Copy-paste how you want your django project folder next to the virtualenv one, for example placing it in the home folder too. I'll use the simple sqlite driver for the database.
Make sure to have a requirements.txt file, listing all needed dependencies. If you've a local working copy of the your django project in your pc, you can generate this file with:
$ pip freeze > requirements.txt
Here's my requirements.txt file as an example. Don't use this, you may have different packages and versions
asgiref==3.4.1
Django==3.2.8
django-ranged-response==0.2.0
django-simple-captcha==0.5.14
gunicorn==20.1.0
Pillow==8.4.0
pytz==2021.3
six==1.16.0
sqlparse==0.4.2
After having copied the project, activate the virtualenv, cd into the project folder and then run
$ pip install -r requirements.txt
.
All dependencies will be installed automatically and resolved.
Now create all the database file and tables automatically with:
$ python manage.py makemigrations
$ python manage.py migrate
For a production environment, make sure to apply these changes to your settings.py file:
Set a random SECRET_KEY used for signing sessions and cookies. Do NOT use the debug one. You can get it from an environment variable. Do NOT versions control it. Edit your project's settings.py file and add
import os
SECRET_KEY = os.environ['SECRET_KEY']
You can generate a secret key with this command. You'll need it in the next steps while configuring the Systemd service. Save it somewhere else securely:
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())
Warning You could theoretically just place this in your settings.py file. The problem of this approach is that it generates a new secret key each time the server starts Django. This means it invalidates every existing cookie and session at each server restart.
## WARNING; MIGHT NOT ME WHAT YOU WANT
import os
from django.core.management.utils import get_random_secret_key
SECRET_KEY = get_random_secret_key()
Disable debug mode to reduce the amount of information returned in responses in case of errors. If Django is in debug mode, it lists the code that crashed and some values helping us troubleshooting our code. If it's running in production mode, it just returns a generic "server error" with an HTTP status code.
DEBUG = False
Add your domain name here. This protects you from some CSRF attacks:
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
In Debug mode, this list is ignored. In production mode, the list is enforced.
If you're using a non-sqlite database, make sure to also get the db password from an environment variable like you did with the SECRET_KEY.
Set STATIC_ROOT and STATIC_URL to tell django where to store static files that will be served by nginx/apache.
Static files can be collected by running
$ python manage.py collectstatic
Gunicorn is a WSGI server written in Python. Make sure to install it into the virtualenv.
After entering the virtualenv, we can install it using:
pip install gunicorn
Remember to save it into the requirements file using pip freeze.
djangoprojectfolder is the internal folder where the settings.py and wsgi.py files are placed. Launch it to test it.
gunicorn -w 4 --bind 0.0.0.0:9000 django_project_folder.wsgi
We need to automated all this stuff so that:
We can do all of that with a systemd service. This is my configuration as an example.
I placed it at /etc/systemd/system/djangogunicorn.service
Here's the sample file, where the "source /home/django/venv/bin/activate;" part activates the virtualenv:
[Unit]
Description=Django using Gunicorn
After=network.target
[Service]
User=django
Group=django
WorkingDirectory=/home/django/django_project_folder
Environment="SECRET_KEY=put_here_your_secret_key_as_environment_variable"
ExecStart=/bin/bash -c 'source /home/django/venv/bin/activate; gunicorn -w 4 --bind 0.0.0.0:9000 django_project_folder.wsgi'
Restart=always
[Install]
WantedBy=multi-user.target
After saving the file, run and see if it runs correctly.
# systemctl start djangogunicorn
See its status with
# systemctl status djangogunicorn
If everything is fine, enable the service at boot by typing:
systemctl enable djangogunicorn
Whenever you modify the .service file, run:
# systemctl daemon-reload
# systemctl restart djangogunicorn
Just setting gunicorn to listen on port 9000 seems to overcome any SeLinux problems. SeLinux already allows communication on this port for network purposes.
In my case, I easily modified the firewall through the cockpit interface (find it at http://server_address:9090/). You can also modify it using a command line.
I have:
On Debians, you can use ufw
$ sudo apt install ufw
$ sudo ufw allow 22/tcp
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp
$ sudo ufw enable