Apache redirects & Django security
Version info
When writing this guide, I used the following:
- A Linode Nanode server with Ubuntu 20.04 LTS
- Apache 2.4.41
- mod_wsgi 4.7.1
- Python version 3.8 (using the venv package for virtual environment management)
- Django version 3.1
- certbot 1.7.0, installed by using
sudo snap install --classic certbot
(as suggested on the certbot website)
Prerequisites
This guide is specifically for people who have gone through Corey M. Schafer's (coreyms) Django tutorial, up to and including "Python Django Tutorial: How to enable HTTPS with a free SSL/TLS Certificate using Let's Encrypt".
I have no affiliation with coreyms and don't know him at all, but I think his tutorial is great and would like to help others who might have had trouble setting up redirects after doing his tutorial.
Aim
In the HTTPS/certification-related video I linked to above, coreyms mentions that in a future video he would like to set things up so that when users go to e. g. 'www.myawesomeapp.com', they'd instead be sent to 'myawesomeapp.com'. I couldn't find any material from him so far on doing this, and I saw in the YouTube comments section for the video that others were also unsure about how to go about it. Also, I noticed that if I tried going to 'http://datalowe.com' (since 'datalowe.com' is the domain name I used), that didn't work at all. I was just shown a warning about how the server was trying to use a certificate that was only valid for 'www.datalowe.com'. So in this guide we'll:
- Acquire an updated/expanded certificate from Let's Encrypt, that also works for when leaving out the 'www.' part.
- Set up redirects so that regardless of whether the user types in 'example.com', 'www.example.com', 'http://example.com', 'http://www.example.com', 'https://example.com' or 'https://www.example.com', they are always sent to 'https://example.com'.
- Configure Django to explicitly tell it to only do things via HTTPS/SSL.
A word of caution
I haven't received any feedback on this approach (please write to me if you have any). I give no guarantees on, and take no responsibility for, whether it will work on your setup, or if it's secure for you to use. It's likely that some of the steps I describe could be skipped or done in a better way. I'm only sharing what happened to work for me. Taking all this together, you should make copies (cp
) of your files before you start editing them, so you can always return to where you started if things are broken.
Starting point
If you've gone through coreyms' tutorial, you should have the following:
- A hosting server (Linode, if using the same as coreyms), that uses Apache2 and has a working Django project
- SSH access to the server, with SSH key login set up for a user with sudo access
- A domain name that you've linked up to your hosting server
- A certificate from Let's Encrypt, acquired by using their 'certbot'
- A configuration file that describes how Apache2 should deal with incoming HTTP requests, say '/etc/apache2/sites-available/django_project.conf', that redirects users to 'https://' addresses
- A configuration file for incoming HTTPS requests, say '/etc/apache2/sites-available/django_project-le-ssl.conf'
If any of the above seems unfamiliar to you, you will probably have to go back and rewatch coreyms' tutorial.
SSH in
We won't be doing anything locally so ssh into your server - you should be familiar with this also from doing the tutorial. Assume that all following commands in this guide are done on the server.
User:~ user$ ssh myusername@100.100.100.100
Stopping the server
This isn't strictly necessary (you could instead use sudo service apache2 restart
once we're done) for any of the steps we follow here, since nothing should come into effect until the server is restarted anyway, as far as I know. But I prefer to shut things down entirely while I'm mucking about with settings until I think everything is good to go again.
username@my-server:~$ sudo service apache2 stop
Acquiring an additional certificate
First let's deal with the issue where anyone going to example.com
instead of www.example.com
gets a message about invalid use of a Let's Encrypt certificate. There is actually very helpful certbot documentation about changing a certificate's domains. Do have a look at that link, because their explanation is super clear. In our case, what we have to do is:
certbot certonly --cert-name www.example.com -d www.example.com, example.com
You should get a message that informs you the certificate has been updated/expanded.
Configuring HTTP request redirects
We use nano to open up our configuration file that describes what should happen for HTTP requests (those coming in through port 80).
user@my-server:~$ sudo nano /etc/apache2/sites-available/django_project.conf
This is what things should look like right now (omitting the irrelevant parts)
<VirtualHost *:80>
# ...
ServerName www.example.com
# ...
#Include conf-available/serve-cgi-bin.conf
RewriteEngine on
RewriteCond %{SERVER_NAME} =www.example.com
RewriteRule ^ %{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
# ...
Do you see those same lines in your .conf file? If not, again, rewatch coreyms' tutorial.
To understand the parts related to "Rewrite...", I found this tutorial by Matt Doyle about mod_rewrite useful. I admit that I didn't read the whole thing yet, but here's a breakdown of what we have, as I understand the parts:
RewriteEngine on
- "Turns on the mod_rewrite rewriting engine.", as Doyle writesRewriteCond
- Describes a condition for when the rewrite (redirect) should happen, similar to anif
statement in Python%{SERVER_NAME}
- Fetches the value of a server variable, which holds the server's hostname. (the%{VAR}
syntax is used in general for referring to server variables)
So RewriteCond %{SERVER_NAME} =www.example.com
means, "if the server variable SERVER_NAME
is equal to 'www.example.com', then...". Moving on to the "then" section:
RewriteRule
- Describes a rule for rewriting one URL to another, here using server variables.^
A regular expression which describes what pattern the request URL should match for the rule to be applied (so sort of like an additional 'if' condition).^
marks the start of what's being matched for, just like with regex in Python (commonly used with there
package). When nothing follows the^
, that means that whatever the URL is, the redirect rule should be applied.%{REQUEST_URI}
- A server variable which holds "the path component of the requested URI, such as '/index.html'." (from Apache's official docs)[END,NE,R=permanent]
- Flags that modify how the rule is applied.END
: Marks this as the final (and only, in our case) rewrite rule that is to be applieadNE
: "noescape", prevents apache from converting special characters (which is otherwise the default), like &, into their hexcode equivalents. So if the user tries to access 'www.example.com/start?title=bar', they'll be sent to 'https://www.example.com/start?title=bar', instead of (with conversion) 'https://www.example.com/start%3ftitle=bar'. That's what we want - the path should be left "as is".R=permanent
, means that when the user is redirected, the 301 (moved permanently) status code is sent. This tells the browser to save this redirect and use it right away. So if the user has been redirected once from 'www.example.com' to 'https://www.example.com', the next time the user tries to go to 'www.example.com' the browser won't even bother going there to get a redirect response, and instead it goes to 'https://www.example.com' straight away.
Now that we understand how the redirects are happening, what's missing? Well, there's nothing in there to say what should happen if the user tries to go to 'example.com' instead of 'www.example.com'. Let's change (using nano) that, and while we're at it, make sure that all redirects (that are sent in response to HTTP requests) go to 'https://example.com'.
<VirtualHost *:80>
# ...
ServerName www.example.com
ServerAlias example.com # new
# ...
#Include conf-available/serve-cgi-bin.conf
RewriteEngine on
RewriteCond %{SERVER_NAME} =www.example.com [OR] # modified
RewriteCond %{SERVER_NAME} =example.com # new
RewriteRule ^ https://example.com%{REQUEST_URI} [END,NE,R=permanent] # modified
</VirtualHost>
# ...
We added an alternative name for our host, using ServerAlias. We also added an alternative condition for our SERVER_NAME
server variable. Similar to how in python we can write if cond1 or cond2:
, we use [OR]
and an additional condition to tell Apache that if either condition is fulfilled, the rewrite rule should be applied. It might seem odd to you that the server name can vary, and I don't entirely understand it myself. But if the user goes to example.com
, then Apache recognizes that as matching the alternate host name we specified, and assigns this to the SERVER_NAME
variable. That's what I think happens - if someone knows better, please do write me so I can correct this.
(sidenote: if you want to read more about [OR]
, you can check out this Stack Overflow thread that helped me understand it)
We changed the When you're done, exit using Now we've ensured that users who go to e. g. 'www.example.com' or 'http://example.com' will be sent to 'https://example.com'. But if someone (for whatever reason) goes to the trouble of writing out the full 'https://www.example.com' address, they won't be redirected. To change this, we use a similar approach as above. Go the .conf file that describes what happens upon HTTPS requests (the one with '-le-ssl' in its name) You should see something like this: There are no redirect rules here at all, so let's add one and change the ServerName, dropping the 'www' part. Hopefully you can figure out why/how this works, based on the steps we worked our way through above. That's it for the Apache configuration files. At a certain point in coreyms' tutorial for using a custom domain name, he modifies the Before we actually modify the 'settings.py' file, let's check what Django thinks we should change to make our Django project more secure. If you haven't already modified your 'settings.py' file after finishing coreyms' tutorial, you'll probably get at least these messages: Thankfully, the Django docs explain very clearly what the messages mean. Here are links to relevant Django docs for each issue: Please read through the above Django docs carefully, so that you understand what each setting relates to and you can make your own informed decision about whether or not you're ready to throw the switches. All of these messages relate to HTTPS/SSL, so we should be able to implement the suggested changes. In the file, change to If you want, and you're certain that you did the previous steps correctly, you can actually try leaving out the You can, if you'd like, now save your changes and exit, then run Now let's add the specifications, and some additional ones related to HSTS, that Django suggested when we ran our check command. You can put put these at the very end of your 'settings.py' file (or wherever you prefer). It's probably wise to add these one by one, restarting your apache2 server inbetween and checking that everything still works okay. Once you're sufficiently confident that the redirects and the site in general are properly set up, you might want to increase the Now try running the check command again. You should find that no issues are raised. If that's not the case, you can consult the Django docs again. Give yourself a pat on the back for not only finishing coreyms' tutorial, but doing these extra steps as well! I am very thankful to coreyms. I'm certain it would have taken me ages to learn how to deploy a Django project using a proper VPS and domain name if I couldn't rely on his series. This guide wouldn't have been possible either, so if you want to support his work, please go to coreyms' website for information on how to do that. If you want to send any feedback or comments, please contact me at datalouvre@gmail.com.RewriteRule
to redirect to 'https://example.com%{REQUEST_URI}'. This means that the REQUEST_URI
server variable will still be used for determining the path component to tack on at the end, but no matter what the value of SERVER_NAME
(e. g. if it's 'www.example.com') is, the user is always sent to the 'example.com' domain. Note that if you ever change your domain names, you must also change this redirect rule, because otherwise 'yourotherwebsite.com' will still redirect your users to 'https://example.com\ctrl+x
and confirm that you want to save the changes.Configuring HTTPS request redirects
user@my-server:~$ sudo nano /etc/apache2/sites-available/django_project-le-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
# ...
ServerName www.example.com
# ...
Alias /static /home/user/django_project/static
<Directory /home/user/django_project/static>
Require all granted
</Directory>
# ...
WSGIScriptAlias / /home/user/django_project/django_project/wsgi.py
WSGIDaemonProcess django_app python-path=/home/user/django_project python-home=/home/user/django_project/venv>
WSGIProcessGroup django_app
SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
<IfModule mod_ssl.c>
<VirtualHost *:443>
# ...
ServerName example.com # modified
# ...
Alias /static /home/user/django_project/static
<Directory /home/user/django_project/static>
Require all granted
</Directory>
# ...
WSGIScriptAlias / /home/user/django_project/django_project/wsgi.py
WSGIDaemonProcess django_app python-path=/home/user/django_project python-home=/home/user/django_project/venv>
WSGIProcessGroup django_app
SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
RewriteEngine on # new
RewriteCond %{SERVER_NAME} =www.example.com # new
RewriteRule ^ https://example.com%{REQUEST_URI} [END,NE,R=permanent] # new
</VirtualHost>
</IfModule>
Reconfiguring django
ALLOWED_HOSTS
setting so that the list also includes his www.myawesomeapp.com
. Unless I missed something, he forgets however to add myawesomeapp.com
. Let's fix this, and also specify some additional security settings.Deployment check
# activate the virtual environment
user@my-server:~$ source django_project/venv/bin/activate
# run the check command with the deploy flag
(venv) user@my-server:~$ python django_project/manage.py check --deploy
System check identified some issues:
WARNINGS:
?: (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.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.
Editing settings.py
(venv) user@my-server:~$ sudo nano django_project/django_project/settings.py
ALLOWED_HOSTS = [
'www.example.com',
'111.222.333.444', # your server's IP address
]
ALLOWED_HOSTS = [
'www.example.com',
'example.com',
'111.222.333.444', # your server's IP address
]
'www.example.com',
part, since our server should be redirecting all users to 'example.com' anyway.Quick check
sudo service apache2 start
and connect to your website using a web browser. See if you are properly redirected, no matter how you try to access the site.SECURE_HSTS_SECONDS = 60
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_SECONDS
setting, as the Docs suggest. Personally I set mine to 86400 seconds, i. e. 24 hours, because I don't know what I'll be changing about the site in the future and I don't want to commit to only running on HTTPS for say a year just yet.(venv) user@my-server:~$ python django_project/manage.py check --deploy
Good job!