Просмотр исходного кода

Fixes #68 - HSTS is now part of the UI

Jamie Curnow 6 лет назад
Родитель
Сommit
2a3d792591

+ 0 - 3
rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf

@@ -7,6 +7,3 @@ ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA
 ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE
 S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
 ssl_prefer_server_ciphers on;
-
-# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
-add_header Strict-Transport-Security max-age=15768000;

+ 21 - 18
src/backend/internal/dead-host.js

@@ -47,6 +47,7 @@ const internalDeadHost = {
             .then(() => {
                 // At this point the domains should have been checked
                 data.owner_user_id = access.token.getUserId(1);
+                data               = internalHost.cleanSslHstsData(data);
 
                 return deadHostModel
                     .query()
@@ -89,11 +90,11 @@ const internalDeadHost = {
 
                 // Add to audit log
                 return internalAuditLog.add(access, {
-                    action:      'created',
-                    object_type: 'dead-host',
-                    object_id:   row.id,
-                    meta:        data
-                })
+                        action:      'created',
+                        object_type: 'dead-host',
+                        object_id:   row.id,
+                        meta:        data
+                    })
                     .then(() => {
                         return row;
                     });
@@ -144,9 +145,9 @@ const internalDeadHost = {
 
                 if (create_certificate) {
                     return internalCertificate.createQuickCertificate(access, {
-                        domain_names: data.domain_names || row.domain_names,
-                        meta:         _.assign({}, row.meta, data.meta)
-                    })
+                            domain_names: data.domain_names || row.domain_names,
+                            meta:         _.assign({}, row.meta, data.meta)
+                        })
                         .then(cert => {
                             // update host with cert id
                             data.certificate_id = cert.id;
@@ -162,7 +163,9 @@ const internalDeadHost = {
                 // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
                 data = _.assign({}, {
                     domain_names: row.domain_names
-                },data);
+                }, data);
+
+                data = internalHost.cleanSslHstsData(data, row);
 
                 return deadHostModel
                     .query()
@@ -171,11 +174,11 @@ const internalDeadHost = {
                     .then(saved_row => {
                         // Add to audit log
                         return internalAuditLog.add(access, {
-                            action:      'updated',
-                            object_type: 'dead-host',
-                            object_id:   row.id,
-                            meta:        data
-                        })
+                                action:      'updated',
+                                object_type: 'dead-host',
+                                object_id:   row.id,
+                                meta:        data
+                            })
                             .then(() => {
                                 return _.omit(saved_row, omissions());
                             });
@@ -183,15 +186,15 @@ const internalDeadHost = {
             })
             .then(() => {
                 return internalDeadHost.get(access, {
-                    id:     data.id,
-                    expand: ['owner', 'certificate']
-                })
+                        id:     data.id,
+                        expand: ['owner', 'certificate']
+                    })
                     .then(row => {
                         // Configure nginx
                         return internalNginx.configure(deadHostModel, 'dead_host', row)
                             .then(new_meta => {
                                 row.meta = new_meta;
-                                row = internalHost.cleanRowCertificateMeta(row);
+                                row      = internalHost.cleanRowCertificateMeta(row);
                                 return _.omit(row, omissions());
                             });
                     });

+ 31 - 2
src/backend/internal/host.js

@@ -1,11 +1,40 @@
-'use strict';
-
+const _                    = require('lodash');
 const proxyHostModel       = require('../models/proxy_host');
 const redirectionHostModel = require('../models/redirection_host');
 const deadHostModel        = require('../models/dead_host');
 
 const internalHost = {
 
+    /**
+     * Makes sure that the ssl_* and hsts_* fields play nicely together.
+     * ie: if there is no cert, then force_ssl is off.
+     *     if force_ssl is off, then hsts_enabled is definitely off.
+     *
+     * @param   {object} data
+     * @param   {object} [existing_data]
+     * @returns {object}
+     */
+    cleanSslHstsData: function (data, existing_data) {
+        existing_data = existing_data === undefined ? {} : existing_data;
+
+        let combined_data = _.assign({}, existing_data, data);
+
+        if (!combined_data.certificate_id) {
+            combined_data.ssl_forced    = false;
+            combined_data.http2_support = false;
+        }
+
+        if (!combined_data.ssl_forced) {
+            combined_data.hsts_enabled = false;
+        }
+
+        if (!combined_data.hsts_enabled) {
+            combined_data.hsts_subdomains = false;
+        }
+
+        return combined_data;
+    },
+
     /**
      * used by the getAll functions of hosts, this removes the certificate meta if present
      *

+ 20 - 19
src/backend/internal/proxy-host.js

@@ -1,5 +1,3 @@
-'use strict';
-
 const _                   = require('lodash');
 const error               = require('../lib/error');
 const proxyHostModel      = require('../models/proxy_host');
@@ -47,6 +45,7 @@ const internalProxyHost = {
             .then(() => {
                 // At this point the domains should have been checked
                 data.owner_user_id = access.token.getUserId(1);
+                data               = internalHost.cleanSslHstsData(data);
 
                 return proxyHostModel
                     .query()
@@ -90,11 +89,11 @@ const internalProxyHost = {
 
                 // Add to audit log
                 return internalAuditLog.add(access, {
-                    action:      'created',
-                    object_type: 'proxy-host',
-                    object_id:   row.id,
-                    meta:        data
-                })
+                        action:      'created',
+                        object_type: 'proxy-host',
+                        object_id:   row.id,
+                        meta:        data
+                    })
                     .then(() => {
                         return row;
                     });
@@ -109,7 +108,7 @@ const internalProxyHost = {
      */
     update: (access, data) => {
         let create_certificate = data.certificate_id === 'new';
-
+console.log('PH UPDATE:', data);
         if (create_certificate) {
             delete data.certificate_id;
         }
@@ -145,9 +144,9 @@ const internalProxyHost = {
 
                 if (create_certificate) {
                     return internalCertificate.createQuickCertificate(access, {
-                        domain_names: data.domain_names || row.domain_names,
-                        meta:         _.assign({}, row.meta, data.meta)
-                    })
+                            domain_names: data.domain_names || row.domain_names,
+                            meta:         _.assign({}, row.meta, data.meta)
+                        })
                         .then(cert => {
                             // update host with cert id
                             data.certificate_id = cert.id;
@@ -165,6 +164,8 @@ const internalProxyHost = {
                     domain_names: row.domain_names
                 }, data);
 
+                data = internalHost.cleanSslHstsData(data, row);
+
                 return proxyHostModel
                     .query()
                     .where({id: data.id})
@@ -172,11 +173,11 @@ const internalProxyHost = {
                     .then(saved_row => {
                         // Add to audit log
                         return internalAuditLog.add(access, {
-                            action:      'updated',
-                            object_type: 'proxy-host',
-                            object_id:   row.id,
-                            meta:        data
-                        })
+                                action:      'updated',
+                                object_type: 'proxy-host',
+                                object_id:   row.id,
+                                meta:        data
+                            })
                             .then(() => {
                                 return _.omit(saved_row, omissions());
                             });
@@ -184,9 +185,9 @@ const internalProxyHost = {
             })
             .then(() => {
                 return internalProxyHost.get(access, {
-                    id:     data.id,
-                    expand: ['owner', 'certificate', 'access_list']
-                })
+                        id:     data.id,
+                        expand: ['owner', 'certificate', 'access_list']
+                    })
                     .then(row => {
                         // Configure nginx
                         return internalNginx.configure(proxyHostModel, 'proxy_host', row)

+ 21 - 18
src/backend/internal/redirection-host.js

@@ -47,6 +47,7 @@ const internalRedirectionHost = {
             .then(() => {
                 // At this point the domains should have been checked
                 data.owner_user_id = access.token.getUserId(1);
+                data               = internalHost.cleanSslHstsData(data);
 
                 return redirectionHostModel
                     .query()
@@ -89,11 +90,11 @@ const internalRedirectionHost = {
 
                 // Add to audit log
                 return internalAuditLog.add(access, {
-                    action:      'created',
-                    object_type: 'redirection-host',
-                    object_id:   row.id,
-                    meta:        data
-                })
+                        action:      'created',
+                        object_type: 'redirection-host',
+                        object_id:   row.id,
+                        meta:        data
+                    })
                     .then(() => {
                         return row;
                     });
@@ -144,9 +145,9 @@ const internalRedirectionHost = {
 
                 if (create_certificate) {
                     return internalCertificate.createQuickCertificate(access, {
-                        domain_names: data.domain_names || row.domain_names,
-                        meta:         _.assign({}, row.meta, data.meta)
-                    })
+                            domain_names: data.domain_names || row.domain_names,
+                            meta:         _.assign({}, row.meta, data.meta)
+                        })
                         .then(cert => {
                             // update host with cert id
                             data.certificate_id = cert.id;
@@ -162,7 +163,9 @@ const internalRedirectionHost = {
                 // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
                 data = _.assign({}, {
                     domain_names: row.domain_names
-                },data);
+                }, data);
+
+                data = internalHost.cleanSslHstsData(data, row);
 
                 return redirectionHostModel
                     .query()
@@ -171,11 +174,11 @@ const internalRedirectionHost = {
                     .then(saved_row => {
                         // Add to audit log
                         return internalAuditLog.add(access, {
-                            action:      'updated',
-                            object_type: 'redirection-host',
-                            object_id:   row.id,
-                            meta:        data
-                        })
+                                action:      'updated',
+                                object_type: 'redirection-host',
+                                object_id:   row.id,
+                                meta:        data
+                            })
                             .then(() => {
                                 return _.omit(saved_row, omissions());
                             });
@@ -183,15 +186,15 @@ const internalRedirectionHost = {
             })
             .then(() => {
                 return internalRedirectionHost.get(access, {
-                    id:     data.id,
-                    expand: ['owner', 'certificate']
-                })
+                        id:     data.id,
+                        expand: ['owner', 'certificate']
+                    })
                     .then(row => {
                         // Configure nginx
                         return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
                             .then(new_meta => {
                                 row.meta = new_meta;
-                                row = internalHost.cleanRowCertificateMeta(row);
+                                row      = internalHost.cleanRowCertificateMeta(row);
                                 return _.omit(row, omissions());
                             });
                     });

+ 53 - 0
src/backend/migrations/20190218060101_hsts.js

@@ -0,0 +1,53 @@
+'use strict';
+
+const migrate_name = 'hsts';
+const logger       = require('../logger').migrate;
+
+/**
+ * Migrate
+ *
+ * @see http://knexjs.org/#Schema
+ *
+ * @param   {Object}  knex
+ * @param   {Promise} Promise
+ * @returns {Promise}
+ */
+exports.up = function (knex/*, Promise*/) {
+    logger.info('[' + migrate_name + '] Migrating Up...');
+
+    return knex.schema.table('proxy_host', function (proxy_host) {
+            proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
+            proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
+        })
+        .then(() => {
+            logger.info('[' + migrate_name + '] proxy_host Table altered');
+
+            return knex.schema.table('redirection_host', function (redirection_host) {
+                redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
+                redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
+            });
+        })
+        .then(() => {
+            logger.info('[' + migrate_name + '] redirection_host Table altered');
+
+            return knex.schema.table('dead_host', function (dead_host) {
+                dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
+                dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
+            });
+        })
+        .then(() => {
+            logger.info('[' + migrate_name + '] dead_host Table altered');
+        });
+};
+
+/**
+ * Undo Migrate
+ *
+ * @param   {Object}  knex
+ * @param   {Promise} Promise
+ * @returns {Promise}
+ */
+exports.down = function (knex, Promise) {
+    logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
+    return Promise.resolve(true);
+};

+ 10 - 0
src/backend/schema/definitions.json

@@ -187,6 +187,16 @@
       "example": false,
       "type": "boolean"
     },
+    "hsts_enabled": {
+      "description": "Is HSTS Enabled",
+      "example": false,
+      "type": "boolean"
+    },
+    "hsts_subdomains": {
+      "description": "Is HSTS applicable to all subdomains",
+      "example": false,
+      "type": "boolean"
+    },
     "ssl_provider": {
       "type": "string",
       "pattern": "^(letsencrypt|other)$"

+ 24 - 0
src/backend/schema/endpoints/dead-hosts.json

@@ -24,6 +24,12 @@
     "ssl_forced": {
       "$ref": "../definitions.json#/definitions/ssl_forced"
     },
+    "hsts_enabled": {
+      "$ref": "../definitions.json#/definitions/hsts_enabled"
+    },
+    "hsts_subdomains": {
+      "$ref": "../definitions.json#/definitions/hsts_subdomains"
+    },
     "http2_support": {
       "$ref": "../definitions.json#/definitions/http2_support"
     },
@@ -56,6 +62,12 @@
     "ssl_forced": {
       "$ref": "#/definitions/ssl_forced"
     },
+    "hsts_enabled": {
+      "$ref": "#/definitions/hsts_enabled"
+    },
+    "hsts_subdomains": {
+      "$ref": "#/definitions/hsts_subdomains"
+    },
     "http2_support": {
       "$ref": "#/definitions/http2_support"
     },
@@ -113,6 +125,12 @@
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
+          "hsts_enabled": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
+          "hsts_subdomains": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
           "http2_support": {
             "$ref": "#/definitions/http2_support"
           },
@@ -153,6 +171,12 @@
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
+          "hsts_enabled": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
+          "hsts_subdomains": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
           "http2_support": {
             "$ref": "#/definitions/http2_support"
           },

+ 24 - 0
src/backend/schema/endpoints/proxy-hosts.json

@@ -38,6 +38,12 @@
     "ssl_forced": {
       "$ref": "../definitions.json#/definitions/ssl_forced"
     },
+    "hsts_enabled": {
+      "$ref": "../definitions.json#/definitions/hsts_enabled"
+    },
+    "hsts_subdomains": {
+      "$ref": "../definitions.json#/definitions/hsts_subdomains"
+    },
     "http2_support": {
       "$ref": "../definitions.json#/definitions/http2_support"
     },
@@ -93,6 +99,12 @@
     "ssl_forced": {
       "$ref": "#/definitions/ssl_forced"
     },
+    "hsts_enabled": {
+      "$ref": "#/definitions/hsts_enabled"
+    },
+    "hsts_subdomains": {
+      "$ref": "#/definitions/hsts_subdomains"
+    },
     "http2_support": {
       "$ref": "#/definitions/http2_support"
     },
@@ -174,6 +186,12 @@
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
+          "hsts_enabled": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
+          "hsts_subdomains": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
           "http2_support": {
             "$ref": "#/definitions/http2_support"
           },
@@ -238,6 +256,12 @@
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
+          "hsts_enabled": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
+          "hsts_subdomains": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
           "http2_support": {
             "$ref": "#/definitions/http2_support"
           },

+ 24 - 0
src/backend/schema/endpoints/redirection-hosts.json

@@ -32,6 +32,12 @@
     "ssl_forced": {
       "$ref": "../definitions.json#/definitions/ssl_forced"
     },
+    "hsts_enabled": {
+      "$ref": "../definitions.json#/definitions/hsts_enabled"
+    },
+    "hsts_subdomains": {
+      "$ref": "../definitions.json#/definitions/hsts_subdomains"
+    },
     "http2_support": {
       "$ref": "../definitions.json#/definitions/http2_support"
     },
@@ -73,6 +79,12 @@
     "ssl_forced": {
       "$ref": "#/definitions/ssl_forced"
     },
+    "hsts_enabled": {
+      "$ref": "#/definitions/hsts_enabled"
+    },
+    "hsts_subdomains": {
+      "$ref": "#/definitions/hsts_subdomains"
+    },
     "http2_support": {
       "$ref": "#/definitions/http2_support"
     },
@@ -140,6 +152,12 @@
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
+          "hsts_enabled": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
+          "hsts_subdomains": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
           "http2_support": {
             "$ref": "#/definitions/http2_support"
           },
@@ -189,6 +207,12 @@
           "ssl_forced": {
             "$ref": "#/definitions/ssl_forced"
           },
+          "hsts_enabled": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
+          "hsts_subdomains": {
+            "$ref": "#/definitions/hsts_enabled"
+          },
           "http2_support": {
             "$ref": "#/definitions/http2_support"
           },

+ 8 - 0
src/backend/templates/_hsts.conf

@@ -0,0 +1,8 @@
+{% if certificate and certificate_id > 0 -%}
+{% if ssl_forced == 1 or ssl_forced == true %}
+{% if hsts_enabled == 1 or hsts_enabled == true %}
+  # HSTS (ngx_http_headers_module is required) (31536000 seconds = 1 year)
+  add_header Strict-Transport-Security "max-age=31536000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload" always;
+{% endif %}
+{% endif %}
+{% endif %}

+ 6 - 1
src/backend/templates/dead_host.conf

@@ -4,11 +4,16 @@
 server {
 {% include "_listen.conf" %}
 {% include "_certificates.conf" %}
+{% include "_hsts.conf" %}
 
   access_log /data/logs/dead_host-{{ id }}.log standard;
 
 {{ advanced_config }}
 
-  return 404;
+  location / {
+{% include "_forced_ssl.conf" %}
+{% include "_hsts.conf" %}
+    return 404;
+  }
 }
 {% endif %}

+ 2 - 0
src/backend/templates/proxy_host.conf

@@ -10,6 +10,7 @@ server {
 {% include "_certificates.conf" %}
 {% include "_assets.conf" %}
 {% include "_exploits.conf" %}
+{% include "_hsts.conf" %}
 
   access_log /data/logs/proxy_host-{{ id }}.log proxy;
 
@@ -23,6 +24,7 @@ server {
     {%- endif %}
 
 {% include "_forced_ssl.conf" %}
+{% include "_hsts.conf" %}
 
     {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
     proxy_set_header Upgrade $http_upgrade;

+ 2 - 2
src/backend/templates/redirection_host.conf

@@ -6,15 +6,15 @@ server {
 {% include "_certificates.conf" %}
 {% include "_assets.conf" %}
 {% include "_exploits.conf" %}
+{% include "_hsts.conf" %}
 
   access_log /data/logs/redirection_host-{{ id }}.log standard;
 
 {{ advanced_config }}
 
-  # TODO: Preserve Path Option
-
   location / {
 {% include "_forced_ssl.conf" %}
+{% include "_hsts.conf" %}
 
     {% if preserve_path == 1 or preserve_path == true %}
         return 301 $scheme://{{ forward_domain_name }}$request_uri;

+ 18 - 0
src/frontend/js/app/nginx/dead/form.ejs

@@ -54,6 +54,24 @@
                                 </label>
                             </div>
                         </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
+                                </label>
+                            </div>
+                        </div>
 
                         <!-- Lets encrypt -->
                         <div class="col-sm-12 col-md-12 letsencrypt">

+ 41 - 9
src/frontend/js/app/nginx/dead/form.js

@@ -22,6 +22,8 @@ module.exports = Mn.View.extend({
         save:               'button.save',
         certificate_select: 'select[name="certificate_id"]',
         ssl_forced:         'input[name="ssl_forced"]',
+        hsts_enabled:       'input[name="hsts_enabled"]',
+        hsts_subdomains:    'input[name="hsts_subdomains"]',
         http2_support:      'input[name="http2_support"]',
         letsencrypt:        '.letsencrypt'
     },
@@ -36,11 +38,44 @@ module.exports = Mn.View.extend({
             }
 
             let enabled = id === 'new' || parseInt(id, 10) > 0;
-            this.ui.ssl_forced.add(this.ui.http2_support)
+
+            let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
+            inputs
                 .prop('disabled', !enabled)
                 .parents('.form-group')
                 .css('opacity', enabled ? 1 : 0.5);
-            this.ui.http2_support.prop('disabled', !enabled);
+
+            if (!enabled) {
+                inputs.prop('checked', false);
+            }
+
+            inputs.trigger('change');
+        },
+
+        'change @ui.ssl_forced': function () {
+            let checked = this.ui.ssl_forced.prop('checked');
+            this.ui.hsts_enabled
+                .prop('disabled', !checked)
+                .parents('.form-group')
+                .css('opacity', checked ? 1 : 0.5);
+
+            if (!checked) {
+                this.ui.hsts_enabled.prop('checked', false);
+            }
+
+            this.ui.hsts_enabled.trigger('change');
+        },
+
+        'change @ui.hsts_enabled': function () {
+            let checked = this.ui.hsts_enabled.prop('checked');
+            this.ui.hsts_subdomains
+                .prop('disabled', !checked)
+                .parents('.form-group')
+                .css('opacity', checked ? 1 : 0.5);
+
+            if (!checked) {
+                this.ui.hsts_subdomains.prop('checked', false);
+            }
         },
 
         'click @ui.save': function (e) {
@@ -55,13 +90,10 @@ module.exports = Mn.View.extend({
             let data = this.ui.form.serializeJSON();
 
             // Manipulate
-            if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
-                data.ssl_forced = true;
-            }
-
-            if (typeof data.http2_support !== 'undefined') {
-                data.http2_support = !!data.http2_support;
-            }
+            data.hsts_enabled    = !!data.hsts_enabled;
+            data.hsts_subdomains = !!data.hsts_subdomains;
+            data.http2_support   = !!data.http2_support;
+            data.ssl_forced      = !!data.ssl_forced;
 
             if (typeof data.domain_names === 'string' && data.domain_names) {
                 data.domain_names = data.domain_names.split(',');

+ 18 - 0
src/frontend/js/app/nginx/proxy/form.ejs

@@ -110,6 +110,24 @@
                                 </label>
                             </div>
                         </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
+                                </label>
+                            </div>
+                        </div>
 
                         <!-- Lets encrypt -->
                         <div class="col-sm-12 col-md-12 letsencrypt">

+ 43 - 9
src/frontend/js/app/nginx/proxy/form.js

@@ -25,6 +25,8 @@ module.exports = Mn.View.extend({
         certificate_select: 'select[name="certificate_id"]',
         access_list_select: 'select[name="access_list_id"]',
         ssl_forced:         'input[name="ssl_forced"]',
+        hsts_enabled:       'input[name="hsts_enabled"]',
+        hsts_subdomains:    'input[name="hsts_subdomains"]',
         http2_support:      'input[name="http2_support"]',
         forward_scheme:     'select[name="forward_scheme"]',
         letsencrypt:        '.letsencrypt'
@@ -40,10 +42,44 @@ module.exports = Mn.View.extend({
             }
 
             let enabled = id === 'new' || parseInt(id, 10) > 0;
-            this.ui.ssl_forced.add(this.ui.http2_support)
+
+            let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
+            inputs
                 .prop('disabled', !enabled)
                 .parents('.form-group')
                 .css('opacity', enabled ? 1 : 0.5);
+
+            if (!enabled) {
+                inputs.prop('checked', false);
+            }
+
+            inputs.trigger('change');
+        },
+
+        'change @ui.ssl_forced': function () {
+            let checked = this.ui.ssl_forced.prop('checked');
+            this.ui.hsts_enabled
+                .prop('disabled', !checked)
+                .parents('.form-group')
+                .css('opacity', checked ? 1 : 0.5);
+
+            if (!checked) {
+                this.ui.hsts_enabled.prop('checked', false);
+            }
+
+            this.ui.hsts_enabled.trigger('change');
+        },
+
+        'change @ui.hsts_enabled': function () {
+            let checked = this.ui.hsts_enabled.prop('checked');
+            this.ui.hsts_subdomains
+                .prop('disabled', !checked)
+                .parents('.form-group')
+                .css('opacity', checked ? 1 : 0.5);
+
+            if (!checked) {
+                this.ui.hsts_subdomains.prop('checked', false);
+            }
         },
 
         'click @ui.save': function (e) {
@@ -63,14 +99,9 @@ module.exports = Mn.View.extend({
             data.caching_enabled         = !!data.caching_enabled;
             data.allow_websocket_upgrade = !!data.allow_websocket_upgrade;
             data.http2_support           = !!data.http2_support;
-
-            if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
-                data.ssl_forced = true;
-            }
-
-            if (typeof data.http2_support !== 'undefined') {
-                data.http2_support = !!data.http2_support;
-            }
+            data.hsts_enabled            = !!data.hsts_enabled;
+            data.hsts_subdomains         = !!data.hsts_subdomains;
+            data.ssl_forced              = !!data.ssl_forced;
 
             if (typeof data.domain_names === 'string' && data.domain_names) {
                 data.domain_names = data.domain_names.split(',');
@@ -132,6 +163,9 @@ module.exports = Mn.View.extend({
     onRender: function () {
         let view = this;
 
+        this.ui.ssl_forced.trigger('change');
+        this.ui.hsts_enabled.trigger('change');
+
         // Domain names
         this.ui.domain_names.selectize({
             delimiter:    ',',

+ 18 - 0
src/frontend/js/app/nginx/redirection/form.ejs

@@ -78,6 +78,24 @@
                                 </label>
                             </div>
                         </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="hsts_enabled" value="1"<%- hsts_enabled ? ' checked' : '' %><%- certificate_id && ssl_forced ? '' : ' disabled' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-enabled') %> <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security" target="_blank"><i class="fe fe-help-circle"></i></a></span>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-sm-6 col-md-6">
+                            <div class="form-group">
+                                <label class="custom-switch">
+                                    <input type="checkbox" class="custom-switch-input" name="hsts_subdomains" value="1"<%- hsts_subdomains ? ' checked' : '' %><%- certificate_id && ssl_forced && hsts_enabled ? '' : ' disabled' %>>
+                                    <span class="custom-switch-indicator"></span>
+                                    <span class="custom-switch-description"><%- i18n('all-hosts', 'hsts-subdomains') %></span>
+                                </label>
+                            </div>
+                        </div>
 
                         <!-- Lets encrypt -->
                         <div class="col-sm-12 col-md-12 letsencrypt">

+ 43 - 12
src/frontend/js/app/nginx/redirection/form.js

@@ -22,6 +22,8 @@ module.exports = Mn.View.extend({
         save:               'button.save',
         certificate_select: 'select[name="certificate_id"]',
         ssl_forced:         'input[name="ssl_forced"]',
+        hsts_enabled:       'input[name="hsts_enabled"]',
+        hsts_subdomains:    'input[name="hsts_subdomains"]',
         http2_support:      'input[name="http2_support"]',
         letsencrypt:        '.letsencrypt'
     },
@@ -36,11 +38,44 @@ module.exports = Mn.View.extend({
             }
 
             let enabled = id === 'new' || parseInt(id, 10) > 0;
-            this.ui.ssl_forced.add(this.ui.http2_support)
+
+            let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
+            inputs
                 .prop('disabled', !enabled)
                 .parents('.form-group')
                 .css('opacity', enabled ? 1 : 0.5);
-            this.ui.http2_support.prop('disabled', !enabled);
+
+            if (!enabled) {
+                inputs.prop('checked', false);
+            }
+
+            inputs.trigger('change');
+        },
+
+        'change @ui.ssl_forced': function () {
+            let checked = this.ui.ssl_forced.prop('checked');
+            this.ui.hsts_enabled
+                .prop('disabled', !checked)
+                .parents('.form-group')
+                .css('opacity', checked ? 1 : 0.5);
+
+            if (!checked) {
+                this.ui.hsts_enabled.prop('checked', false);
+            }
+
+            this.ui.hsts_enabled.trigger('change');
+        },
+
+        'change @ui.hsts_enabled': function () {
+            let checked = this.ui.hsts_enabled.prop('checked');
+            this.ui.hsts_subdomains
+                .prop('disabled', !checked)
+                .parents('.form-group')
+                .css('opacity', checked ? 1 : 0.5);
+
+            if (!checked) {
+                this.ui.hsts_subdomains.prop('checked', false);
+            }
         },
 
         'click @ui.save': function (e) {
@@ -55,16 +90,12 @@ module.exports = Mn.View.extend({
             let data = this.ui.form.serializeJSON();
 
             // Manipulate
-            data.block_exploits = !!data.block_exploits;
-            data.preserve_path  = !!data.preserve_path;
-
-            if (typeof data.ssl_forced !== 'undefined' && data.ssl_forced === '1') {
-                data.ssl_forced = true;
-            }
-
-            if (typeof data.http2_support !== 'undefined') {
-                data.http2_support = !!data.http2_support;
-            }
+            data.block_exploits  = !!data.block_exploits;
+            data.preserve_path   = !!data.preserve_path;
+            data.http2_support   = !!data.http2_support;
+            data.hsts_enabled    = !!data.hsts_enabled;
+            data.hsts_subdomains = !!data.hsts_subdomains;
+            data.ssl_forced      = !!data.ssl_forced;
 
             if (typeof data.domain_names === 'string' && data.domain_names) {
                 data.domain_names = data.domain_names.split(',');

+ 3 - 1
src/frontend/js/i18n/messages.json

@@ -79,7 +79,9 @@
       "no-ssl": "This host will not use HTTPS",
       "advanced": "Advanced",
       "advanced-warning": "Enter your custom Nginx configuration here at your own risk!",
-      "advanced-config": "Custom Nginx Configuration"
+      "advanced-config": "Custom Nginx Configuration",
+      "hsts-enabled": "HSTS Enabled",
+      "hsts-subdomains": "HSTS Subdomains"
     },
     "ssl": {
       "letsencrypt": "Let's Encrypt",

+ 2 - 0
src/frontend/js/models/dead-host.js

@@ -14,6 +14,8 @@ const model = Backbone.Model.extend({
             certificate_id:  0,
             ssl_forced:      false,
             http2_support:   false,
+            hsts_enabled:    false,
+            hsts_subdomains: false,
             enabled:         true,
             meta:            {},
             advanced_config: '',

+ 2 - 0
src/frontend/js/models/proxy-host.js

@@ -17,6 +17,8 @@ const model = Backbone.Model.extend({
             access_list_id:          0,
             certificate_id:          0,
             ssl_forced:              false,
+            hsts_enabled:            false,
+            hsts_subdomains:         false,
             caching_enabled:         false,
             allow_websocket_upgrade: false,
             block_exploits:          false,

+ 2 - 0
src/frontend/js/models/redirection-host.js

@@ -15,6 +15,8 @@ const model = Backbone.Model.extend({
             preserve_path:       true,
             certificate_id:      0,
             ssl_forced:          false,
+            hsts_enabled:        false,
+            hsts_subdomains:     false,
             block_exploits:      false,
             http2_support:       false,
             advanced_config:     '',