diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index f2e845a2..a27f8180 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -576,6 +576,7 @@ const internalCertificate = { return internalCertificate.create(access, { provider: 'letsencrypt', domain_names: data.domain_names, + ssl_key_type: data.ssl_key_type, meta: data.meta }); }, @@ -838,6 +839,7 @@ const internalCertificate = { const cmd = `${certbotCommand} certonly ` + `--config '${letsencryptConfig}' ` + + `--key-type '${certificate.ssl_key_type}' ` + '--work-dir "/tmp/letsencrypt-lib" ' + '--logs-dir "/tmp/letsencrypt-log" ' + `--cert-name "npm-${certificate.id}" ` + @@ -879,6 +881,7 @@ const internalCertificate = { let mainCmd = certbotCommand + ' certonly ' + `--config '${letsencryptConfig}' ` + + `--key-type '${certificate.ssl_key_type}' ` + '--work-dir "/tmp/letsencrypt-lib" ' + '--logs-dir "/tmp/letsencrypt-log" ' + `--cert-name 'npm-${certificate.id}' ` + @@ -975,6 +978,7 @@ const internalCertificate = { const cmd = certbotCommand + ' renew --force-renewal ' + `--config '${letsencryptConfig}' ` + + `--key-type '${certificate.ssl_key_type}' ` + '--work-dir "/tmp/letsencrypt-lib" ' + '--logs-dir "/tmp/letsencrypt-log" ' + `--cert-name 'npm-${certificate.id}' ` + @@ -1008,6 +1012,7 @@ const internalCertificate = { let mainCmd = certbotCommand + ' renew --force-renewal ' + `--config "${letsencryptConfig}" ` + + `--key-type '${certificate.ssl_key_type}' ` + '--work-dir "/tmp/letsencrypt-lib" ' + '--logs-dir "/tmp/letsencrypt-log" ' + `--cert-name 'npm-${certificate.id}' ` + @@ -1038,9 +1043,10 @@ const internalCertificate = { */ revokeLetsEncryptSsl: (certificate, throw_errors) => { logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - + const mainCmd = certbotCommand + ' revoke ' + `--config '${letsencryptConfig}' ` + + `--key-type '${certificate.ssl_key_type}' ` + '--work-dir "/tmp/letsencrypt-lib" ' + '--logs-dir "/tmp/letsencrypt-log" ' + `--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` + diff --git a/backend/internal/host.js b/backend/internal/host.js index 52c6d2bd..747c9018 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -229,8 +229,32 @@ const internalHost = { } return response; - } + }, + /** + * Internal use only, checks to see if the there is another default server record + * + * @param {String} hostname + * @param {String} [ignore_type] 'proxy', 'redirection', 'dead' + * @param {Integer} [ignore_id] Must be supplied if type was also supplied + * @returns {Promise} + */ + checkDefaultServerNotExist: function (hostname) { + let promises = proxyHostModel + .query() + .where('default_server', true) + .andWhere('domain_names', 'not like', '%' + hostname + '%'); + + + return Promise.resolve(promises) + .then((promises_results) => { + if (promises_results.length > 0){ + return false; + } + return true; + }); + + } }; module.exports = internalHost; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 32f2bc0d..8bb528de 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -44,6 +44,22 @@ const internalProxyHost = { }); }); }) + .then(() => { + // Get a list of the domain names and check each of them against default records + if (data.default_server){ + if (data.domain_names.length > 1) { + throw new error.ValidationError('Default server cant be set for multiple domain!'); + } + + return internalHost + .checkDefaultServerNotExist(data.domain_names[0]) + .then((result) => { + if (!result){ + throw new error.ValidationError('One default server already exists'); + } + }); + } + }) .then(() => { // At this point the domains should have been checked data.owner_user_id = access.token.getUserId(1); @@ -141,6 +157,22 @@ const internalProxyHost = { }); } }) + .then(() => { + // Get a list of the domain names and check each of them against default records + if (data.default_server){ + if (data.domain_names.length > 1) { + throw new error.ValidationError('Default server cant be set for multiple domain!'); + } + + return internalHost + .checkDefaultServerNotExist(data.domain_names[0]) + .then((result) => { + if (!result){ + throw new error.ValidationError('One default server already exists'); + } + }); + } + }) .then(() => { return internalProxyHost.get(access, {id: data.id}); }) @@ -153,6 +185,7 @@ const internalProxyHost = { if (create_certificate) { return internalCertificate.createQuickCertificate(access, { domain_names: data.domain_names || row.domain_names, + ssl_key_type: data.ssl_key_type || row.ssl_key_type, meta: _.assign({}, row.meta, data.meta) }) .then((cert) => { diff --git a/backend/migrations/20241209062244_ssl_key_type.js b/backend/migrations/20241209062244_ssl_key_type.js new file mode 100644 index 00000000..647b2c71 --- /dev/null +++ b/backend/migrations/20241209062244_ssl_key_type.js @@ -0,0 +1,51 @@ +const migrate_name = 'identifier_for_migrate'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex) { + + logger.info(`[${migrate_name}] Migrating Up...`); + + return knex.schema.alterTable('proxy_host', (table) => { + table.enum('ssl_key_type', ['ecdsa', 'rsa']).defaultTo('ecdsa').notNullable(); + }).then(() => { + logger.info(`[${migrate_name}] Column 'ssl_key_type' added to table 'proxy_host'`); + + return knex.schema.alterTable('certificate', (table) => { + table.enum('ssl_key_type', ['ecdsa', 'rsa']).defaultTo('ecdsa').notNullable(); + }); + }).then(() => { + logger.info(`[${migrate_name}] Column 'ssl_key_type' added to table 'proxy_host'`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info(`[${migrate_name}] Migrating Down...`); + + return knex.schema.alterTable('proxy_host', (table) => { + table.dropColumn('ssl_key_type'); + }).then(() => { + logger.info(`[${migrate_name}] Column 'ssl_key_type' removed from table 'proxy_host'`); + + return knex.schema.alterTable('certificate', (table) => { + table.dropColumn('ssl_key_type'); + }); + }).then(() => { + logger.info(`[${migrate_name}] Column 'ssl_key_type' removed from table 'proxy_host'`); + }); +}; diff --git a/backend/migrations/20241221201400_default_server.js b/backend/migrations/20241221201400_default_server.js new file mode 100644 index 00000000..177f03b2 --- /dev/null +++ b/backend/migrations/20241221201400_default_server.js @@ -0,0 +1,40 @@ +const migrate_name = 'default_server'; +const logger = require('../logger').migrate; + +/** + * Migrate Up + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex) { + logger.info(`[${migrate_name}] Migrating Up...`); + + // Add default_server column to proxy_host table + return knex.schema.table('proxy_host', (table) => { + table.boolean('default_server').notNullable().defaultTo(false); + }) + .then(() => { + logger.info(`[${migrate_name}] Column 'default_server' added to 'proxy_host' table`); + }); +}; + +/** + * Migrate Down + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info(`[${migrate_name}] Migrating Down...`); + + // Remove default_server column from proxy_host table + return knex.schema.table('proxy_host', (table) => { + table.dropColumn('default_server'); + }) + .then(() => { + logger.info(`[${migrate_name}] Column 'default_server' removed from 'proxy_host' table`); + }); +}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 07aa5dd3..5a9595b1 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -21,6 +21,7 @@ const boolFields = [ 'enabled', 'hsts_enabled', 'hsts_subdomains', + 'default_server', ]; class ProxyHost extends Model { diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json index b75dcf61..301de87a 100644 --- a/backend/schema/components/certificate-object.json +++ b/backend/schema/components/certificate-object.json @@ -41,6 +41,11 @@ "owner": { "$ref": "./user-object.json" }, + "ssl_key_type": { + "type": "string", + "enum": ["ecdsa", "rsa"], + "description": "Type of SSL key (either ecdsa or rsa)" + }, "meta": { "type": "object", "additionalProperties": false, diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index e9dcacb5..bbfcd4ad 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -22,6 +22,7 @@ "enabled", "locations", "hsts_enabled", + "default_server", "hsts_subdomains" ], "additionalProperties": false, @@ -148,6 +149,15 @@ "$ref": "./access-list-object.json" } ] + }, + "ssl_key_type": { + "type": "string", + "enum": ["ecdsa", "rsa"], + "description": "Type of SSL key (either ecdsa or rsa)" + }, + "default_server": { + "type": "boolean", + "description": "Defines if the server is the default for unmatched requests" } } } diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index 5cab6e75..d1c5974e 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -79,6 +79,12 @@ }, "locations": { "$ref": "../../../../components/proxy-host-object.json#/properties/locations" + }, + "ssl_key_type": { + "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_key_type" + }, + "default_server": { + "$ref": "../../../../components/proxy-host-object.json#/properties/default_server" } } } diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index 85455fb6..2343bb5b 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -67,6 +67,12 @@ }, "locations": { "$ref": "../../../components/proxy-host-object.json#/properties/locations" + }, + "ssl_key_type": { + "$ref": "../../../components/proxy-host-object.json#/properties/ssl_key_type" + }, + "default_server": { + "$ref": "../../../components/proxy-host-object.json#/properties/default_server" } } } diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf index 34a808e6..53358678 100644 --- a/backend/templates/_listen.conf +++ b/backend/templates/_listen.conf @@ -1,13 +1,13 @@ - listen 80; +listen 80{% if default_server == true %} default_server{% endif %}; {% if ipv6 -%} - listen [::]:80; + listen [::]:80{% if default_server == true %} default_server{% endif %}; {% else -%} #listen [::]:80; {% endif %} {% if certificate -%} - listen 443 ssl; + listen 443 ssl{% if default_server == true %} default_server{% endif %}; {% if ipv6 -%} - listen [::]:443 ssl; + listen [::]:443 ssl{% if default_server == true %} default_server{% endif %}; {% else -%} #listen [::]:443; {% endif %} diff --git a/docker/Dockerfile b/docker/Dockerfile index 0603e2de..f74a6799 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -53,9 +53,11 @@ COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager # Remove frontend service not required for prod, dev nginx config as well RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \ && chmod 644 /etc/logrotate.d/nginx-proxy-manager +COPY docker/start-container /usr/local/bin/start-container +RUN chmod +x /usr/local/bin/start-container VOLUME [ "/data" ] -ENTRYPOINT [ "/init" ] +ENTRYPOINT [ "start-container" ] LABEL org.label-schema.schema-version="1.0" \ org.label-schema.license="MIT" \ diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index dcb1f1f9..2b47ae11 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -36,5 +36,8 @@ RUN rm -f /etc/nginx/conf.d/production.conf \ COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt +COPY start-container /usr/local/bin/start-container +RUN chmod +x /usr/local/bin/start-container + EXPOSE 80 81 443 -ENTRYPOINT [ "/init" ] +ENTRYPOINT [ "start-container" ] diff --git a/docker/dev/letsencrypt.ini b/docker/dev/letsencrypt.ini index 93647b64..a960ceaa 100644 --- a/docker/dev/letsencrypt.ini +++ b/docker/dev/letsencrypt.ini @@ -1,7 +1,5 @@ text = True non-interactive = True webroot-path = /data/letsencrypt-acme-challenge -key-type = ecdsa -elliptic-curve = secp384r1 preferred-chain = ISRG Root X1 server = diff --git a/docker/rootfs/etc/letsencrypt.ini b/docker/rootfs/etc/letsencrypt.ini index aae53b90..ec23efdb 100644 --- a/docker/rootfs/etc/letsencrypt.ini +++ b/docker/rootfs/etc/letsencrypt.ini @@ -1,6 +1,4 @@ text = True non-interactive = True webroot-path = /data/letsencrypt-acme-challenge -key-type = ecdsa -elliptic-curve = secp384r1 preferred-chain = ISRG Root X1 diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf index b5dacfb5..a7a35e26 100644 --- a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf @@ -1,4 +1,6 @@ # intermediate configuration. tweak to your needs. ssl_protocols TLSv1.2 TLSv1.3; -ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; +ssl_ciphers "ALL:RC4-SHA:AES128-SHA:AES256-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384:AES128-GCM-SHA256:RSA-AES256-CBC-SHA:RC4-MD5:DES-CBC3-SHA:AES256-SHA:RC4-SHA:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; ssl_prefer_server_ciphers off; +ssl_ecdh_curve X25519:prime256v1:secp384r1; +ssl_dhparam /etc/ssl/certs/dhparam.pem; \ No newline at end of file diff --git a/docker/start-container b/docker/start-container new file mode 100644 index 00000000..859575a2 --- /dev/null +++ b/docker/start-container @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +FILE="/etc/ssl/certs/dhparam.pem" + +if [ ! -f "$FILE" ]; then + echo "the $FILE does not exist, creating..." + openssl dhparam -out "$FILE" 2048 +else + echo "the $FILE already exists, skipping..." +fi + +echo "run default script" +exec /init \ No newline at end of file diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 8e7a2a2d..ba37a734 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -72,7 +72,7 @@ -
+
+
+
+ +
+
@@ -105,6 +114,15 @@
+
+
+ + +
+