Security is a massively important aspect of deployment for production. In this section, we will work on securing our dockerized django application. As we have done throughout this series, we will use automation to make our lives better.
Setting up SSL
We will use letsencrypt for our SSL certificate and use certbot to enable automatic renewal of the SSL certificate provided by letsencrypt.
Prerequisites
- A domain name
- A running virtual machine (You can use any cloud service provider you want AWS, GCP, Digital Ocean, Linode or anything you want) where you have deployed your application on http.
- You have added a DNS A record that points your domain to your remote machine's ip where you have deployed your application. If you don't know what this means, just contact your domain name provider.
Note: Deploying a dockerized application like the one we built throughout this series is very easy. You just install docker and docker-compose on the remote machine and start up your container. DONE π
Step 1: Enable 443 port on our nginx container
This can be done easily inside our docker-compose like so,
proxy:
build:
context: ./proxy
volumes:
- static_data_prod_volume:/vol/static
restart: always
ports:
- "80:80"
- "443:443"
depends_on:
- app
Step 2: Update proxy Dockerfile
If you remember, we kep't our nginx container related files inside the proxy directory.
proxy/Dockerfile
FROM nginx:stable
COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY uwsgi_params /etc/nginx/uwsgi_params
USER root
RUN apt-get update
RUN apt-get install -y --no-install-recommends certbot python3-certbot-nginx
RUN mkdir -p /vol/static
RUN chmod 755 /vol/static
RUN useradd user
USER user
# RUN certbot --nginx --email "${CERTBOT_EMAIL}" --agree-tos --no-eff-email -d "${DOMAIN_LIST}"
Note 1: We are installing two new packages certbot and python3-certbot-nginx. The first one will give us access to the certbot cli and the second one is the nginx plugin for certbot which will update our nginx configuration files for https.
Note 2: Check the last commented out line. There are many online blogs i read that tell you to run that command during the time of building your container for automation. But frankly, it would not work. Because, that command sends ACME challenges to your application running on http. But, when this container is on the process of being built, it cannot answer the ACME challenges. So, the challenges fail. So, we have to run the command when the container is up and running and ready to answer the challenges.
Step 3: Update nginx configuration file
proxy/default.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name your-domain.com;
location /static {
alias /vol/static;
}
location / {
uwsgi_pass app:8000;
include /etc/nginx/uwsgi_params;
}
add_header Strict-Transport-Security max-age=31536000 always;
gzip on;
gzip_comp_level 2;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types application/x-javascript application/javascript application/xml application/json text/xml text/css text;
client_max_body_size 10M;
client_body_timeout 12;
client_header_timeout 12;
reset_timedout_connection on;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
expires 1y;
access_log off;
log_not_found off;
}
There are quite a few things being done here and while some of them are not strictly necessary, its still considered good practice. You might wonder, we are not really doing anything specific to https here like redirecting http traffic to https. Then how will it work? Dont worry that part will automatically be handled by certbot plugin.
Step 4: Run the nginx container
Our nginx container is ready for https and we can now run the container. Just ssh into your remote machine (where you have deployed your application) and run the container like so,
sudo docker-compose up -d --build proxy
Step 5: Invoke Certbot
Now, we will execute the certbot command we commented out before inside our proxy container.
sudo docker ps # first find the container id for the proxy container
sudo docker exec -it container_id bash # now, it will open a bash session inside our nginx container
certbot --nginx --email xyz@xyz.com --agree-tos --no-eff-email -d www.mydomain.com
You should see some logs regarding the ACME challenges being sent and once they are completed successfully, you can just exit out of the container.
Now, if you hit your domain with https in any browser, your application should respond properly.
BONUS
If you want to monitor what the certbot plugin writes to your nginx configuration file, you can easily inspect it by opening a bash session inside your proxy container and inspect the file /etc/nginx/conf.d/default.conf like so,
cat /etc/nginx/conf.d/default.conf
server {
server_name mydomain.com;
location /static {
alias /vol/static;
}
location / {
uwsgi_pass app:8000;
include /etc/nginx/uwsgi_params;
}
add_header Strict-Transport-Security max-age=31536000 always;
gzip on;
gzip_comp_level 2;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types application/x-javascript application/javascript application/xml application/json text/xml text/css text;
client_max_body_size 10M;
client_body_timeout 12;
client_header_timeout 12;
reset_timedout_connection on;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
expires 1y;
access_log off;
log_not_found off;
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/mydomain.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 = mydomain.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80 default_server;
listen [::]:80 default_server;
server_name mydomain.com;
return 404; # managed by Certbot
}
Note: Check the lines with the comment # managed by Certbot. The location of your letsencrypt ssl certificate can be found under the ssl_certificate value added by Certbot. Also, check the server {} block at the end that returns an HTTP 301 (redirect) to all requests on HTTP (port 80) towards HTTPS (port 443).
Step 6: Automating SSL certificate renewal
We can do this easily using cron in our remote machine. We just have to put the following command on a crontab.
sudo docker exec container_id bash -c "/usr/bin/certbot renew --quiet"
Lets, say we kept this command on a script called update-ssl.sh and if we want to check for renewal on Sundays and Thursdays,
30 23 * * 0,4 /bin/bash /path-to/update-ssl.sh
Now, that our application has https enabled, lets dive into some other security checks for django.
manage.py check --deploy
Django has a deployment checklist which provide the status of your application's protection against some common vulnerabilities.
python manage.py check --deploy
If you run this command on a newly created django app, it will show the following output
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems.
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS.
?: (security.W009) Your SECRET_KEY has less than 50 characters, less than 5 unique characters, or it's prefixed with 'django-insecure-' indicating that it was generated automatically by Django. Please generate a long and random SECRET_KEY, otherwise many of Django's security-critical features will be vulnerable to attack.
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions.
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.
?: (security.W018) You should not have DEBUG set to True in deployment.
?: (security.W020) ALLOWED_HOSTS must not be empty in deployment
The Security Middleware
If you look at the middlewares section in your application's settings.py file, you should see the django.middleware.security.SecurityMiddleware (by default). This middleware provides several security enhancements for your django application. These can be configured directly from your settings.py file.
Since, we enabled https, we can now enable HSTS (HTTP Strict Transport Policy) like so,
if not DEBUG: # we only need to do these in production
SECURE_HSTS_SECONDS = 86400
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
Now, browsers will refuse to connect to our application for the specified SECURE_HSTS_SECONDS if our application is not serving https and/or our certificate expires.
Next, we have SECURE_SSL_REDIRECT. But, we dont need this since, we already use nginx to redirect non-https traffic to https.
if not DEBUG:
SECURE_HSTS_SECONDS = 86400
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# SECURE_SSL_REDIRECT = True
Finally, we have SECURE_CONTENT_TYPE_NOSNIFF and SECURE_BROWSER_XSS_FILTER
if not DEBUG:
SECURE_HSTS_SECONDS = 86400
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# SECURE_SSL_REDIRECT = True
SECURE_BROWSER_XSS_FILTER = True # sets the X-XSS-Protection: 1; mode=block header on all responses and protects from XSS attacks
SECURE_CONTENT_TYPE_NOSNIFF = True # sets the X-Content-Type-Options: nosniff header on responses and protects from content sniffing where no mimetype is sent
With that we are done configuring the security middleware. There are a few more settings that you can look into here. But, they have sensible defaults.
The CsrfViewMiddleware & the XFrameOptionsMiddleware
Learn how the CsrfViewMiddleware protects against common CSRF attacks here.
if not DEBUG:
SECURE_HSTS_SECONDS = 86400
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# SECURE_SSL_REDIRECT = True
SECURE_BROWSER_XSS_FILTER = True # sets the X-XSS-Protection: 1; mode=block header on all responses and protects from XSS attacks
SECURE_CONTENT_TYPE_NOSNIFF = True # sets the X-Content-Type-Options: nosniff header on responses and protects from content sniffing where no mimetype is sent
CSRF_COOKIE_SECURE = True # stop transmition of the CSRF cookie over HTTP
SESSION_COOKIE_SECURE = True # stop transmition of the session cookie over HTTP
X_FRAME_OPTIONS = 'DENY' # or 'SAMEORIGIN, this will prevent loading your site on iframes and protect from potential clickjacking attacks
Note 1: It is possible to allow some views to disable csrf protection if you want using the csrf_exempt decorator. Read more on this Here
Note 2: It is possible to allow some views to run on frames if you want using the @xframe_options_exempt decorator. Read more on this Here
The CORS Middleware
A typical use case of django is when it hosts a DRF rest api or a graphene graphql api and some other frontend application consumes it from a different machine. In this situation, its important to enable Cross Origin Resource Sharing (CORS) for our application.
In order to do that, we can leverage the CORSMiddleware provided by django-cors-headers package.
pip install django-cors-headers
Now, we need to register the corsheaders app in our list of INSTALLED_APPS like so,
INSTALLED_APPS = [
...
'corsheaders',
]
and also register the CorsMiddleware
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Now, all we need to do is setup our CORS whitelist in our settings.py like so,
if os.environ.get('CORS_WHITELIST'):
CORS_ORIGIN_WHITELIST = os.environ.get('CORS_WHITELIST').split(',')
elif DEBUG:
CORS_ORIGIN_ALLOW_ALL = True
As we have done throughout this series, we want to keep secrets like these in our .live.env file that we feed to our docker container.
DJANGO_APP_SECRET_KEY=blah_blah_bleh
AWS_ACCESS_KEY_ID=blah_bleh_blah
AWS_SECRET_ACCESS_KEY=blah_blah_bleh
AWS_STORAGE_BUCKET_NAME=blah_blah_bleh
MYSQL_DATABASE=blah_blah_bleh
MYSQL_ROOT_PASSWORD=blah_blah_bleh
MYSQL_USER=blah_blah_bleh
MYSQL_PASSWORD=blah_blah_bleh
CORS_WHITELIST=https://www.myfrontendappdomain.com,https://www.myotherfrontendappdomain.com
Miscellaneous Good Practices
If you don't think this post is not unreasonably long already, do take a loot at the some of the following:
Keep secrets like the Django SECRET_KEY, DATABASE params, ALLOWED_HOSTS, CORS_WHITELIST, other 3rd party API KEYS in environment variables instead of hard coding them in your source code.
Never run applications in production with DEBUG=True setting. If you do, attackers can gain access to critical info (e.g: environment variables) that can totally sabotage your site.
Modern Django versions require that you set ALLOWED_HOSTS explicitly instead of relying on your web server configuration. Django uses the host header to construct urls in some cases but when it does that it performs host header validation which checks if the specified host is indeed in the ALLOWED_HOSTS list. So, if attackers use a fake Host header and try to perform CSRF and Cache poisoning attacks, they will fail. However, there is a critical back door that you need to remember. If your code needs to read the host header manually, never do it from the django django.http.HttpRequest object's META property (request.META), do it using the django.http.HttpRequest.get_host() method. If the host does not match any in the ALLOWED_HOSTS, this will raise a django.core.exceptions.DisallowedHost exception and protect you from a fake host header.
Secure user uploaded files !!! If your application accepts file uploads, you should configure your web server to keep these requests to a reasonable size to prevent against DOS attacks. Notice, how we did this in our nginx configuration by specifying the client_max_body_size to 10M. Furthermore, it is strongly recommended to not host and serve static/media files through django. A good practice would be to upload files to a Storage Solution Provider like AWS S3 and serve them through a CDN like CLOUD FRONT.
Do not share your django SECRET_KEY with anyone. Django uses this key to generate cryptographic hashes for many high level features. The security of these features heavily rely on this secret to be an actual SECRET xD Django places additional emphasis on this by naming this SECRET_KEY instead of sth like CRYPT_SALT (which would have been more appropriate based on what it does).
By default, Django does not throttle requests to authenticate users. You should consider using DRF-Throttling for your django rest framework views.
Protect from SQL Injection !!!. Django's querysets use query parameterization. So, if you always use the django-orm to build your sql, you should be safe from SQL injection attacks. However, django allows writing RawSQL and Extra which are vulnerable to SQL injection attacks. While i do agree there can be use cases where a RawSQL might perform better than the django orm generated one, i would highly recommend not using the extra() queryset modifier if possible. Nevertheless, if you use any of these you should be extra careful on how you build your raw sql strings with user provided data.
Run dockerized applications with a non-root user.
Try to stay updated to the latest version of python packages including the django version. I recommend using the latest LTS release. Its a good idea to write your requirements.txt with ranges like: Django>=3.2,<3.3 to make sure you get all the patch updates while ensuring you don't welcome any breaking changes whenever you build your app. This is important because often these patches contain security fixes that you don't want to miss out. You can keep an eye on the evolution of security vulnerabilities regarding django versions here and you will know exactly what i mean π.
You should consider setting up application level load balancers to avoid single points of failure in your application. Furthermore, application level load balancers can protect from some common DDoS attacks (e.g: SYN FLOOD).
Protect from DDoS !!!. You can implement many levels of protection against DDoS and we can talk about this for hours :) If you are using AWS, you can look at this very well written Whitepaper and setup DDoS protection depending on the scope of your application.
Thats all! Phew! This might be the longest thing i have written on the internet, but do keep in mind that you should not take things lightly when it comes to security π Let me know, if i missed anything π
Top comments (0)