-
Notifications
You must be signed in to change notification settings - Fork 7
Configuring SSL for NGINX
These are some quick instructions on how to configure SSL for the Production Stack. Keep in mind that the Production Stack is different in terms of hosting compared to the Development Stack. It uses its own instance of Nginx to serve the edx content.
#Overview Edx is composed mainly of the LMS (Student View) and the CMS (Studio View). These are considered their own apps/sites so edx splits them up as sites using different nginx configuration files.
The configuration files are usually located within /edx/app/nginx/sites-available and are labeled as cms and lms
upstream lms-backend {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
# LMS configuration file for nginx, templated by ansible
listen 80 default;
listen 48000 default ssl;
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# request the browser to use SSL for all connections
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
access_log /edx/var/log/nginx/access.log;
error_log /edx/var/log/nginx/error.log error;
# CS184 requires uploads of up to 4MB for submitting screenshots.
# CMS requires larger value for course assest, values provided
# via hiera.
client_max_body_size 4M;
rewrite ^(.*)/favicon.ico$ /static/images/favicon.ico last;
location @proxy_to_lms_app {
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://lms-backend;
}
location / {
try_files $uri @proxy_to_lms_app;
}
# No basic auth security on the github_service_hook url, so that github can use it for cms
location /github_service_hook {
try_files $uri @proxy_to_lms_app;
}
# No basic auth security on the heartbeat url, so that ELB can use it
location /heartbeat {
try_files $uri @proxy_to_lms_app;
}
# Check security on this
location ~ ^/static/(?P<file>.*) {
root /edx/var/edxapp;
try_files /staticfiles/$file /course_static/$file =404;
# return a 403 for static files that shouldn't be
# in the staticfiles directory
location ~ ^/static/(?:.*)(?:\.xml|\.json|README.TXT) {
return 403;
}
# http://www.red-team-design.com/firefox-doesnt-allow-cross-domain-fonts-by-default
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\.(eot|otf|ttf|woff))" {
expires max;
add_header Access-Control-Allow-Origin *;
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files to maximum cache time
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\..*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Expire other static files immediately (there should be very few / none of these)
expires epoch;
}
# Forward to HTTPS if we're an HTTP request...
if ($http_x_forwarded_proto = "http") {
set $do_redirect "true";
}
# Run our actual redirect...
if ($do_redirect = "true") {
rewrite ^ https://$host$request_uri? permanent;
}
}
upstream cms-backend {
server 127.0.0.1:8010 fail_timeout=0;
}
server {
# CMS configuration file for nginx, templated by ansible
listen 18010 ;
listen 48010 ssl;
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# request the browser to use SSL for all connections
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
server_name ~^((stage|prod)-)?studio.*;
access_log /edx/var/log/nginx/access.log;
error_log /edx/var/log/nginx/error.log error;
# CS184 requires uploads of up to 4MB for submitting screenshots.
# CMS requires larger value for course assest, values provided
# via hiera.
client_max_body_size 100M;
rewrite ^(.*)/favicon.ico$ /static/images/favicon.ico last;
location @proxy_to_cms_app {
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://cms-backend;
}
location / {
try_files $uri @proxy_to_cms_app;
}
# No basic auth security on the github_service_hook url, so that github can use it for cms
location /github_service_hook {
try_files $uri @proxy_to_cms_app;
}
# No basic auth security on the heartbeat url, so that ELB can use it
location /heartbeat {
try_files $uri @proxy_to_cms_app;
}
# Check security on this
location ~ ^/static/(?P<file>.*) {
root /edx/var/edxapp;
try_files /staticfiles/$file /course_static/$file =404;
# return a 403 for static files that shouldn't be
# in the staticfiles directory
location ~ ^/static/(?:.*)(?:\.xml|\.json|README.TXT) {
return 403;
}
# http://www.red-team-design.com/firefox-doesnt-allow-cross-domain-fonts-by-default
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\.(eot|otf|ttf|woff))" {
expires max;
add_header Access-Control-Allow-Origin *;
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files to maximum cache time
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\..*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Expire other static files immediately (there should be very few / none of these)
expires epoch;
}
# Forward to HTTPS if we're an HTTP request...
if ($http_x_forwarded_proto = "http") {
set $do_redirect "true";
}
# Run our actual redirect...
if ($do_redirect = "true") {
rewrite ^ https://$host$request_uri? permanent;
}
}
SSL configuration is a straightforward process that requires some changes to the default nginx configurations above.
An SSL certificate must be created or obtained in order to configure nginx to allow ssl. Typically the configuration is obtained through ITS or whoever is the server owner. Both the certificate '.crt' and key '.key' files are required to configure nginx with ssl.
We need to force HTTPS by creating a redirect from HTTP to HTTPS. This is done by creating a new server block within nginx configuration and telling it to redirect everything from port 80 to return a https url.
server {
listen 80;
return 301 https://$host$request_uri;
}
Studio does not require a redirect since it is on a port which is not port 80.
Now we need to tell the original generated server block to use ssl and point the configuration to wherever the certificate and key files reside on the server. LMS will use port 443 by default as it is the default HTTPS port.
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/online-dev-cdot.crt;
ssl_certificate_key /etc/nginx/ssl/online-dev-cdot-insecure.key;
listen 18010 ssl;
ssl_certificate /etc/nginx/ssl/online-dev-cdot.crt;
ssl_certificate_key /etc/nginx/ssl/online-dev-cdot-insecure.key;
Here is what the end result should look like with the above updates.
# LMS configuration file for nginx, templated by ansible
upstream lms-backend {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
# error pages
error_page 504 /server/server-error.html;
error_page 502 /server/server-error.html;
error_page 500 /server/server-error.html;
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/online-dev-cdot.crt;
ssl_certificate_key /etc/nginx/ssl/online-dev-cdot-insecure.key;
access_log /edx/var/log/nginx/access.log p_combined;
error_log /edx/var/log/nginx/error.log error;
# CS184 requires uploads of up to 4MB for submitting screenshots.
# CMS requires larger value for course assest, values provided
# via hiera.
client_max_body_size 4M;
rewrite ^(.*)/favicon.ico$ /static/images/favicon.ico last;
location @proxy_to_lms_app {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://lms-backend;
}
location / {
try_files $uri @proxy_to_lms_app;
}
# No basic auth for /segmentio/event
location /segmentio/event {
try_files $uri @proxy_to_lms_app;
}
location /notifier_api {
try_files $uri @proxy_to_lms_app;
}
# No basic auth security on the github_service_hook url, so that github can use it for cms
location /github_service_hook {
try_files $uri @proxy_to_lms_app;
}
# No basic auth security on oath2 endpoint
location /oauth2 {
try_files $uri @proxy_to_lms_app;
}
# No basic auth security on the heartbeat url, so that ELB can use it
location /heartbeat {
try_files $uri @proxy_to_lms_app;
}
location /courses { try_files $uri @proxy_to_lms_app;
}
# static pages for server status
location ~ ^/server/(?P<file>.*) {
root /edx/var/nginx/server-static;
try_files /$file =404;
}
location ~ ^/static/(?P<file>.*) {
root /edx/var/edxapp;
try_files /staticfiles/$file /course_static/$file =404;
# return a 403 for static files that shouldn't be
# in the staticfiles directory
location ~ ^/static/(?:.*)(?:\.xml|\.json|README.TXT) {
return 403;
}
# http://www.red-team-design.com/firefox-doesnt-allow-cross-domain-fonts-by-default
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\.(eot|otf|ttf|woff))" {
expires max;
add_header Access-Control-Allow-Origin *;
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files to maximum cache time
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\..*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files for studio to maximum cache time
location ~ "/static/(?P<collected>[0-9a-f]{7}/.*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Expire other static files immediately (there should be very few / none of these)
expires epoch;
}
# Forward to HTTPS if we're an HTTP request...
if ($http_x_forwarded_proto = "http") {
set $do_redirect "true";
}
# Run our actual redirect...
if ($do_redirect = "true") {
rewrite ^ https://$host$request_uri? permanent;
}
}
upstream cms-backend {
server 127.0.0.1:8010 fail_timeout=0;
}
server {
# CMS configuration file for nginx, templated by ansible
# Proxy to a remote maintanence page
# error pages
error_page 504 /server/server-error.html;
error_page 502 /server/server-error.html;
error_page 500 /server/server-error.html;
listen 18010 ssl;
ssl_certificate /etc/nginx/ssl/online-dev-cdot.crt;
ssl_certificate_key /etc/nginx/ssl/online-dev-cdot-insecure.key;
server_name ~^((stage|prod)-)?studio.*;
access_log /edx/var/log/nginx/access.log p_combined;
error_log /edx/var/log/nginx/error.log error;
# CS184 requires uploads of up to 4MB for submitting screenshots.
# CMS requires larger value for course assest, values provided
# via hiera.
client_max_body_size 100M;
rewrite ^(.*)/favicon.ico$ /static/images/favicon.ico last;
location @proxy_to_cms_app {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Ssl on;
proxy_redirect off;
proxy_pass http://cms-backend;
}
location / {
try_files $uri @proxy_to_cms_app;
}
# No basic auth security on the github_service_hook url, so that github can use it for cms
location /github_service_hook {
try_files $uri @proxy_to_cms_app;
}
# No basic auth security on the heartbeat url, so that ELB can use it
location /heartbeat {
try_files $uri @proxy_to_cms_app;
}
# static pages for server status
location ~ ^/server/(?P<file>.*) {
root /edx/var/nginx/server-static;
try_files /$file =404;
}
location ~ ^/static/(?P<file>.*) {
root /edx/var/edxapp;
try_files /staticfiles/$file /course_static/$file =404;
# return a 403 for static files that shouldn't be
# in the staticfiles directory
location ~ ^/static/(?:.*)(?:\.xml|\.json|README.TXT) {
return 403;
}
# http://www.red-team-design.com/firefox-doesnt-allow-cross-domain-fonts-by-default
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\.(eot|otf|ttf|woff))" {
expires max;
add_header Access-Control-Allow-Origin *;
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files to maximum cache time
location ~ "/static/(?P<collected>.*\.[0-9a-f]{12}\..*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Set django-pipelined files for studio to maximum cache time
location ~ "/static/(?P<collected>[0-9a-f]{7}/.*)" {
expires max;
# Without this try_files, files that have been run through
# django-pipeline return 404s
try_files /staticfiles/$collected /course_static/$collected =404;
}
# Expire other static files immediately (there should be very few / none of these)
expires epoch;
}
# Forward to HTTPS if we're an HTTP request...
if ($http_x_forwarded_proto = "http") {
set $do_redirect "true";
}
# Run our actual redirect...
if ($do_redirect = "true") {
rewrite ^ https://$host$request_uri? permanent;
}
}
Keep in mind, anytime the server is reinstalled or updated it will wipe the configurations so always keep a backup of your nginx configurations. Open edX uses Ansible to generate the nginx configurations when building the server so it will rewrite. It seems like the Ansible script has limits to SSL configurations, it can be enabled but things such as redirect and changing ports isn't simple to do. This document will be updated if a better Ansible solution is found.
Also keep in mind that nginx wants the crt file to be in pem (text) not binary (der) format. If you get an error restarting nginx, check that your crt file format and convert if it needed. This Article provides the details.