Securing a Django Site in Production
I was setting up a Django site for somebody recently and got asked the question, “is it possible for someone to hack my site?”. The answer, of course, is yes. To some degree, this is unavoidable. If somebody is willing to expend the time, effort and money, it is almost impossible to have a complex site that is perfectly secure. Even security “experts” can get it wrong. However, this got me thinking about the steps to secure a Django site.
Django does a good job of being reasonably secure by default. Unlike some other frameworks where you have to explicitly use CSRF tokens, Django uses them unless you tell it not to. Django escapes data from your templates automatically and is generally safe from SQL injection. The framework contains the building blocks to build a secure site, but quite often the site is deployed on a shaky foundation.
Securing the admin
For maximum security, the Django admin site should probably always be deployed on a web server running HTTPS. There’s a good guide on setting up SSL for the admin. Redirecting requests for /admin to HTTPS is one way. Another way is setup the admin on a subdomain like admin.example.com and handle them like that. This is what it looks like in Nginx:
server {
listen 80;
server_name example.com www.example.com;
...
}
server {
listen 443;
server_name admin.example.com;
ssl_certificate sslcert.crt;
ssl_certificate_key sslcert.key;
...
}
Using this, you can proxy to two different Django instances: one handles the site over HTTP and one handles just the admin over HTTPS. Depending on your exact setup, you probably also want to mark the cookie as secure.
While the admin always needs security, some sites could also benefit from security outside of the admin if they’re handling user details, email addresses or other things. As an application developer, you need to build in that security — Django doesn’t know what you need to protect. Just remember the next time you login to your Django admin screen on a wifi hotspot at Starbucks that anybody can run something like Firesheep or Wireshark and capture your credentials. It’s amazing how many notable sites get this wrong. It reminds me of the wall of sheep.
Securing the server
It is amazing how many people put out a server with an inadequate firewall. Either they leave their database port wide open, memcached port open (this is REALLY bad — see here) or in some other way greatly increase the possible attack surface. While I generally knew what Amazon Web Services (AWS) could do as far as hosting, I had never used them before recently and I was impressed by their security. AWS makes configuring the firewall super easy and by default, only port 22 is open and SSH only accepts keys not passwords. That’s fairly secure by default! It gives a simple web GUI to open select ports and only to select machines. For example, if you host your database on a different server than your web server, only the web server should be able to connect to the database, not the whole internet. Also, Amazon S3 can serve its files over HTTPS as well. It’s a rather handy feature. I expect Rackspace is fairly similar in most regards.
Django security update
There were a couple fixes and changes in Django 1.2.5, but the main change was to CSRF exceptions to AJAX requests. The decision to remove the exception — despite backwards incompatibility — was the right move considering that the assumption that XmlHttpRequests could only come from the browser is no longer true (was it ever?). However, this release makes me wonder how many site authors didn’t bother to change much and just put @csrf_exempt above their web services just to get their site working again quickly with the new version.
Note: I secured the wordpress admin using the guide here and the WordPress HTTPS plugin. It’s a self-signed cert so I’m only getting maybe 75% of the security pixie dust, but I can deal with that.
Edit (September 14, 2011): Take a look through the Django security docs which your humble blogger helped write.
Subversion Backups via Email
I wrote a simple subversion backup script that runs on my hosting provider, webfaction, and backs up my subversion repository. I have this script running in cron and sending periodic backups to gmail. However, it should work on any unix based system with python, gzip, and subversion. Simply set the SMTP settings (configured in the webfaction panel if you have webfaction), your email information and the path to subversion and run the script.
This script requires python 2.5 or 2.6. It will require modification under python 2.4.
!/usr/bin/env python2.6
import os
import smtplib
import time
import mimetypes
from datetime import datetime
# python 2.5/2.6 is required because the email library changed after 2.4
from email import encoders
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
# SMTP settings
SMTPUSER = 'YOURSMTPUSERNAME'
SMTPPASS = 'YOURSMTPPASSOWRD'
SMTPSERVER = 'smtp.webfaction.com'
SMTPPORT = 25
# who to address the email to and from
TO = 'YOUREMAIL@EXAMPLE.com'
FROM = 'backups@webfaction.com'
# svn location
SVN_LOCATION = 'PATH_TO_SVN_DIR'
# these probably do not need to be changed
SVN_BACKUP_FILE = 'svn.dump'
GZIP_BACKUP_FILE = SVN_BACKUP_FILE + '.gz'
BACKUP_LOCATION = '/tmp/'+SVN_BACKUP_FILE
ZIPPED_BACKUP = BACKUP_LOCATION+'.gz'
BACKUP_CMD = 'svnadmin dump '+SVN_LOCATION+' > ' + BACKUP_LOCATION
GZIP_CMD = 'gzip -f '+BACKUP_LOCATION
print '*******************************************************'
print '** Backing up repository'
print '*******************************************************'
os.system(BACKUP_CMD)
print '*******************************************************'
print '** Zipping backup'
print '*******************************************************'
os.system(GZIP_CMD)
print '*******************************************************'
print '** Emailing backup'
print '*******************************************************'
msg = MIMEMultipart()
msg['Subject'] = 'Subversion Backup '+str(datetime.now())
msg['From'] = FROM
msg['To'] = list().append(TO)
msg.preample = 'Should not see this in a MIME-aware mail reader.\n'
# add the gzipped attachment
fp = open(ZIPPED_BACKUP, 'rb')
att = MIMEBase('application', 'gzip')
att.set_payload(fp.read())
encoders.encode_base64(att)
att.add_header('Content-Disposition', 'attachment', filename=GZIP_BACKUP_FILE)
fp.close()
msg.attach(att)
# Send the email via our own SMTP server.
s = smtplib.SMTP(SMTPSERVER, SMTPPORT)
s.ehlo()
s.starttls()
s.ehlo()
s.login(SMTPUSER, SMTPPASS)
s.sendmail(FROM, TO, msg.as_string())
s.quit()
# removing backup
os.remove(ZIPPED_BACKUP)
In order to use gmail’s smtp server, change your settings like so:
SMTPUSER = 'YOURGMAILUSERNAME'
SMTPPASS = 'YOURGMAILPASSOWRD'
SMTPSERVER = 'smtp.gmail.com'
SMTPPORT = 587 # note the port change!
