Skip to content

Configuring SSL for NGINX

Mark Fischer edited this page Aug 5, 2017 · 3 revisions

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

Default LMS and CMS nginx configurations

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;
        }
      }

CMS

    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;
      }

    }

Configuring SSL

SSL configuration is a straightforward process that requires some changes to the default nginx configurations above.

Obtain SSL Certificates

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.

Changes to the nginx configuration files

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.

LMS

      server {
          listen 80;
          return 301 https://$host$request_uri;
      }

CMS

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.

LMS

        listen 443 ssl;
        ssl_certificate /etc/nginx/ssl/online-dev-cdot.crt;
        ssl_certificate_key /etc/nginx/ssl/online-dev-cdot-insecure.key;

CMS

        listen 18010 ssl;
        ssl_certificate /etc/nginx/ssl/online-dev-cdot.crt;
        ssl_certificate_key /etc/nginx/ssl/online-dev-cdot-insecure.key;

Updated Configuration Files

Here is what the end result should look like with the above updates.

LMS

      # 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;
      }
    }

CMS

    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;
      }

    }

Notes

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.

Clone this wiki locally