From bd919c788521f605981cdffcadfd6e351dc8d705 Mon Sep 17 00:00:00 2001 From: luthor Date: Tue, 17 Oct 2017 16:24:55 +0200 Subject: [PATCH 1/4] add letsencrypt support --- .gitignore | 1 + README.md | 45 +++++++++++++++++++++++++++++++++++++++- app.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9afdc52..3ee8fb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ npm-debug.log *.DS_Store node_modules +package-lock.json tests/nginx/* tests/test.json* tests/mechanic-overrides diff --git a/README.md b/README.md index 0a38217..ac31638 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ Let's add a single proxy that talks to one node process, which is listening on p *All commands must be run as root.* +**(Optional) Step Four:** + +Install letsencrypt for automatic host certification or configure your own certificate provider. + ## Adding a site ``` @@ -149,6 +153,46 @@ Next we decide we want the site to be secure all the time, redirecting any traff mechanic update mysite --https=true --redirect-to-https=true ``` +## Certifying a secure site + +You may use a certificate authority to automatically get SSL certificates for the hostnames on your site. + +`letsencrypt` is included by default. + +You may add more providers as shell scripts in `/etc/nginx/mechanic-certproviders`. + +The files can be nunjucks templates, where the following variables are available: + +`hosts`: array of hostnames configured for your site, including aliases +`name`: the name of your site +`host`: your site's default host + +To automatically generate certificates for a site, add the `--certify` flag to your site configuration, like so: + +``` +mechanic update mysite --certify=true +``` +or +``` +mechanic add mysite --host=example.com --aliases=example1.com,example2.com --https=true --certify=true +``` + +To change the global certificate provider, run +``` +mechanic set certProvider myCertProvider +``` +where `myCertProvider` is the name of the file you've created in `/etc/nginx/mechanic-certproviders`. + +You may change the certificate provider per site. + +To do so, run +``` +mechanic update mysite --certProvider=myCertProvider --certify +``` + +For the default letsencrypt provider, certificates are symlinked to the configuration folder, +so you can use `letsencrypt renew` to renew all your certificates automatically. + ## Shutting off HTTPS Now we've decided we don't want ecommerce anymore. Let's shut that off: @@ -341,4 +385,3 @@ Killed support for `tlsv1` as it is insecure. 0.1.1, 0.1.2: `reset` command works. 0.1.0: initial release. - diff --git a/app.js b/app.js index 1bb5178..4edda28 100644 --- a/app.js +++ b/app.js @@ -4,6 +4,7 @@ var fs = require('fs'); var shelljs = require('shelljs'); var resolve = require('path').resolve; var shellEscape = require('shell-escape'); +const debug = false; var dataFile; if (argv.data) { @@ -15,20 +16,29 @@ if (argv.data) { // have a /var/lib/misc folder for storage of "state files // that don't need a directory." But create it if it's // somehow missing (Mac for instance). - if (!fs.existsSync('/var/lib/misc')) { + if (!debug && !fs.existsSync('/var/lib/misc')) { fs.mkdirSync('/var/lib/misc', 0700); } } +const certProviders = { + letsencrypt: 'letsencrypt certonly --standalone {% for host in hosts %}-d {{host}} {% endfor %}; ln -s /etc/letsencrypt/live/{{host}}/fullchain.pem /etc/nginx/certs/{{name}}.cer; ln -s /etc/letsencrypt/live/{{host}}/privkey.pem /etc/nginx/certs/{{name}}.key' +} + var data = require('prettiest')({ json: dataFile }); var defaultSettings = { conf: '/etc/nginx/conf.d', + certProviders: '/etc/nginx/mechanic-certproviders', overrides: '/etc/nginx/mechanic-overrides', logs: '/var/log/nginx', restart: 'nginx -s reload', - bind: '*' + bind: '*', + autoCert: 'false', + certProvider: 'letsencrypt', + preConfig: 'service nginx stop', + postConfig: 'service nginx start' }; _.defaults(data, { settings: {} }); @@ -36,6 +46,10 @@ _.defaults(data.settings, defaultSettings); var settings = data.settings; +if (!debug && !fs.existsSync(settings.certProviders)) { + fs.mkdirSync(settings.certProviders); +} + var nunjucks = require('nunjucks'); var command = argv._[0]; @@ -56,7 +70,9 @@ var options = { 'static': 'string', 'autoindex': 'boolean', 'https': 'boolean', - 'redirect-to-https': 'boolean' + 'redirect-to-https': 'boolean', + 'certify': 'boolean', + 'certProvider': 'string' }; var parsers = { @@ -179,6 +195,11 @@ function update(add) { var shortname = argv._[1]; var site; + if(settings.preConfig) { + if(shelljs.exec(settings.preConfig).code !== 0) { + console.error('ERROR: failed to run preConfig script: \'' + settings.preConfig + '\'.'); + } + } if (add) { if (findSite(shortname)) { usage('Site already exists, use update'); @@ -213,6 +234,38 @@ function update(add) { } }); + let _hardProviders = debug?[]:fs.readdirSync(settings.certProviders), + hardProviders = {}; + for(const provider of _hardProviders) { + hardProviders[provider] = fs.readFileSync(`${settings.certProviders}/${provider}`).toString(); + } + let providers = { + ...certProviders, + ...hardProviders + } + + if(site.certify) { + let _provider = site.certProvider || settings.certProvider; + let provider = providers[_provider]; + + if(provider) { + let result = 0; + let parts = provider.replace(/\n/g,';').split(';'); + for(const part of parts) { + result |= shelljs.exec(nunjucks.renderString(part, { + hosts: [site.host, ...site.aliases], + name: site.shortname, + host: site.host + })).code; + } + if(result) console.error('ERROR: certProvider ' + _provider + ' terminated with non-zero exit code.'); + } + } + if(settings.postConfig) { + if(shelljs.exec(settings.postConfig).code !== 0) { + console.error('ERROR: failed to run postConfig script: \'' + settings.postConfig + '\'.'); + } + } go(); } @@ -284,7 +337,7 @@ function go() { }); }); - fs.writeFileSync(settings.conf + '/mechanic.conf', output); + if(!debug) fs.writeFileSync(settings.conf + '/mechanic.conf', output); if (settings.restart !== false) { var restart = settings.restart || 'service nginx reload'; From a2bdfcc8ee3f3a57dfdfa5029a4e4340b7059042 Mon Sep 17 00:00:00 2001 From: luthor Date: Tue, 17 Oct 2017 16:26:05 +0200 Subject: [PATCH 2/4] fixed alias lookup --- app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 4edda28..18bb9b6 100644 --- a/app.js +++ b/app.js @@ -247,13 +247,14 @@ function update(add) { if(site.certify) { let _provider = site.certProvider || settings.certProvider; let provider = providers[_provider]; + let aliases = site.aliases||[]; if(provider) { let result = 0; let parts = provider.replace(/\n/g,';').split(';'); for(const part of parts) { result |= shelljs.exec(nunjucks.renderString(part, { - hosts: [site.host, ...site.aliases], + hosts: [site.host, ...aliases], name: site.shortname, host: site.host })).code; From 5b7356f44fc81527202769d22a49f1d2eec1e7e1 Mon Sep 17 00:00:00 2001 From: luthor Date: Thu, 7 Dec 2017 22:49:35 +0100 Subject: [PATCH 3/4] removed ES6 depdence --- app.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/app.js b/app.js index 18bb9b6..6b6132f 100644 --- a/app.js +++ b/app.js @@ -4,7 +4,7 @@ var fs = require('fs'); var shelljs = require('shelljs'); var resolve = require('path').resolve; var shellEscape = require('shell-escape'); -const debug = false; +var debug = false; var dataFile; if (argv.data) { @@ -21,7 +21,7 @@ if (argv.data) { } } -const certProviders = { +var certProviders = { letsencrypt: 'letsencrypt certonly --standalone {% for host in hosts %}-d {{host}} {% endfor %}; ln -s /etc/letsencrypt/live/{{host}}/fullchain.pem /etc/nginx/certs/{{name}}.cer; ln -s /etc/letsencrypt/live/{{host}}/privkey.pem /etc/nginx/certs/{{name}}.key' } @@ -234,27 +234,25 @@ function update(add) { } }); - let _hardProviders = debug?[]:fs.readdirSync(settings.certProviders), + var _hardProviders = debug?[]:fs.readdirSync(settings.certProviders), hardProviders = {}; - for(const provider of _hardProviders) { + for(var provider of _hardProviders) { hardProviders[provider] = fs.readFileSync(`${settings.certProviders}/${provider}`).toString(); } - let providers = { - ...certProviders, - ...hardProviders - } + var providers = {}; + Object.assign(providers, certProviders, hardProviders); if(site.certify) { - let _provider = site.certProvider || settings.certProvider; - let provider = providers[_provider]; - let aliases = site.aliases||[]; + var _provider = site.certProvider || settings.certProvider; + var provider = providers[_provider]; + var aliases = site.aliases||[]; if(provider) { - let result = 0; - let parts = provider.replace(/\n/g,';').split(';'); - for(const part of parts) { + var result = 0; + var parts = provider.replace(/\n/g,';').split(';'); + for(var part of parts) { result |= shelljs.exec(nunjucks.renderString(part, { - hosts: [site.host, ...aliases], + hosts: [site.host].concat(aliases), name: site.shortname, host: site.host })).code; From 0ef4d7b2dbc1d9a11c78e5910d0fa32c72a87c24 Mon Sep 17 00:00:00 2001 From: luthor Date: Thu, 7 Dec 2017 23:02:22 +0100 Subject: [PATCH 4/4] add sanity checks --- app.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 6b6132f..8a139c6 100644 --- a/app.js +++ b/app.js @@ -2,7 +2,8 @@ var argv = require('boring')(); var _ = require('lodash'); var fs = require('fs'); var shelljs = require('shelljs'); -var resolve = require('path').resolve; +var path = require('path'); +var resolve = path.resolve; var shellEscape = require('shell-escape'); var debug = false; @@ -36,11 +37,15 @@ var defaultSettings = { restart: 'nginx -s reload', bind: '*', autoCert: 'false', - certProvider: 'letsencrypt', + certProvider: null, preConfig: 'service nginx stop', postConfig: 'service nginx start' }; +if(shelljs.exec('which letsencrypt').code === 0) { + defaultSettings.certProvider = 'letsencrypt'; +} + _.defaults(data, { settings: {} }); _.defaults(data.settings, defaultSettings); @@ -237,13 +242,13 @@ function update(add) { var _hardProviders = debug?[]:fs.readdirSync(settings.certProviders), hardProviders = {}; for(var provider of _hardProviders) { - hardProviders[provider] = fs.readFileSync(`${settings.certProviders}/${provider}`).toString(); + hardProviders[provider] = fs.readFileSync(path.join(settings.certProviders,provider)).toString(); } var providers = {}; Object.assign(providers, certProviders, hardProviders); - if(site.certify) { - var _provider = site.certProvider || settings.certProvider; + var _provider = site.certProvider || settings.certProvider; + if(site.certify && _provider) { var provider = providers[_provider]; var aliases = site.aliases||[];