From b2407029ab8effbc245975342f78c910c74bc8c5 Mon Sep 17 00:00:00 2001 From: Ben Schmidt Date: Sat, 8 Oct 2016 15:16:26 +1100 Subject: [PATCH 1/2] support tls-sni-01 challenge Previously the http-01 challenge was simply served over SSL. --- README.md | 18 +++++++++++++++--- index.js | 29 ++++++++++++++++++++++++----- lib/servers.js | 2 +- package.json | 1 + 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8f46de7..d45b0e0 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,9 @@ multiple domains doesn't work for you, file a bug. ### Standalone -You can run standalone mode to get a cert **on the server** you will be -using it for over ports **80 and 443 (or 5001)** like so: +You can run standalone mode to get a cert **on the server**. You either use an +http-01 challenge (the default) on port 80, or a tls-sni-01 challenge on port +443 (or 5001). Like so: ```bash letsencrypt certonly \ @@ -60,6 +61,17 @@ letsencrypt certonly \ --config-dir ~/letsencrypt/etc ``` +or + +```bash +letsencrypt certonly \ + --agree-tos --email john.doe@example.com \ + --standalone --tls-sni-01-port 443 \ + --domains example.com,www.example.com \ + --server https://acme-staging.api.letsencrypt.org/directory \ + --config-dir ~/letsencrypt/etc +``` + Then you can see your certs at `~/letsencrypt/etc/live`. ``` @@ -174,7 +186,7 @@ Options: --debug BOOLEAN show traces and logs - --tls-sni-01-port NUMBER Use TLS-SNI-01 challenge type with this port. (Default is 443) + --tls-sni-01-port NUMBER Use TLS-SNI-01 challenge type with this port. (must be 443 with most production servers) (Boulder allows 5001 in testing mode) --http-01-port [NUMBER] Use HTTP-01 challenge type with this port, used for SimpleHttp challenge. (Default is 80) diff --git a/index.js b/index.js index f69f989..1c40f1b 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ module.exports.run = function (args) { args.standalone = USE_DNS; } else if (args.tlsSni01Port) { challengeType = 'tls-sni-01'; + args.webrootPath = ''; } else /*if (args.http01Port)*/ { challengeType = 'http-01'; } @@ -27,12 +28,13 @@ module.exports.run = function (args) { // TODO rename le-challenge-fs to le-challenge-webroot leChallenge = require('./lib/webroot').create({ webrootPath: args.webrootPath }); } + else if (args.tlsSni01Port) { + leChallenge = require('le-challenge-sni').create({}); + servers = require('./lib/servers').create(leChallenge); + } else if (USE_DNS !== args.standalone) { leChallenge = require('le-challenge-standalone').create({}); - servers = require('./lib/servers').create(leChallenge).startServers( - args.http01Port || [80], args.tlsSni01Port || [443, 5001] - , { debug: args.debug } - ); + servers = require('./lib/servers').create(leChallenge); } leStore = require('le-store-certbot').create({ @@ -51,14 +53,31 @@ module.exports.run = function (args) { } // let LE know that we're handling standalone / webroot here + var leChallenges = {}; + leChallenges[challengeType] = leChallenge; var le = LE.create({ debug: args.debug , server: args.server , store: leStore - , challenges: { 'http-01': leChallenge, 'tls-sni-01': leChallenge } + , challenges: leChallenges , duplicate: args.duplicate }); + if (servers) { + if (args.tlsSni01Port) { + servers = servers.startServers( + [], args.tlsSni01Port + , { debug: args.debug, httpsOptions: le.httpsOptions } + ); + } + else { + servers = servers.startServers( + args.http01Port || [80], [] + , { debug: args.debug } + ); + } + } + // Note: can't use args directly as null values will overwrite template values le.register({ domains: args.domains diff --git a/lib/servers.js b/lib/servers.js index 1c2b7a1..fcae91f 100644 --- a/lib/servers.js +++ b/lib/servers.js @@ -25,7 +25,7 @@ module.exports.create = function (challenge) { , startServers: function (plainPorts, tlsPorts, opts) { opts = opts || {}; - var httpsOptions = require('localhost.daplie.com-certificates'); + var httpsOptions = opts.httpsOptions || require('localhost.daplie.com-certificates'); var https = require('https'); var http = require('http'); diff --git a/package.json b/package.json index f160371..d23dd87 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "homedir": "^0.6.0", "le-acme-core": "^2.0.5", "le-challenge-manual": "^2.0.0", + "le-challenge-sni": "^2.0.0", "le-challenge-standalone": "^2.0.0", "le-store-certbot": "^2.0.2", "letsencrypt": "^2.1.2", From 2f1b5771175cbf37baa27cefd3e57a83c7219982 Mon Sep 17 00:00:00 2001 From: Ben Schmidt Date: Sun, 9 Oct 2016 18:31:18 +1100 Subject: [PATCH 2/2] support Apache/other TLS SNI server integration --- README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++-- bin/letsencrypt.js | 14 ++++++- index.js | 15 ++++++- package.json | 1 + 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d45b0e0..b1a96be 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Want to use the **live server**? **Note**: This has really only been tested with single domains so if multiple domains doesn't work for you, file a bug. -### Standalone +### Standalone (primarily for testing) You can run standalone mode to get a cert **on the server**. You either use an http-01 challenge (the default) on port 80, or a tls-sni-01 challenge on port @@ -82,9 +82,10 @@ This option is great for testing, but since it requires the use of the same ports that your webserver needs, it isn't a good choice for production. -### WebRoot (for production) +### WebRoot (production option 1) -You can specify the path to where you keep your `index.html` with `webroot`. +You can specify the path to where you keep your `index.html` with `webroot`, as +long as your server is serving plain HTTP on port 80. For example, if I want to get a domain for `example.com` and my `index.html` is at `/srv/www/example.com`, then I would use this command: @@ -110,6 +111,67 @@ ls /etc/letsencrypt/live/ You can use a cron job to run the script above every 80 days (the certificates expire after 90 days) so that you always have fresh certificates. +### TLS SNI (production option 2) + +You can also integrate with a secure server. This is more complicated than the +webroot option, but it allows you to obtain certificates with only port 443 +open. This facility was developed for the Apache webserver, but it could work +with other servers as long as they support server name indication (SNI) and you +can provide a configuration file template and hooks to install and uninstall it +(without downtime). In fact, it doesn't even need to be a webserver (though it +must run on port 443); it could be another server that performs SSL/TLS +negotiation with SNI. + +The process works something like this. You would run: + +```bash +sudo letsencrypt certonly \ + --agree-tos --email john.doe@example.com \ + --apache \ + --config-dir /etc/letsencrypt \ + --domains example.com,www.example.com \ + --server https://acme-staging.api.letsencrypt.org/directory +``` + +Three files are then generated: + +* a configuration fragment: `some-long-string.conf` +* a challenge-fulfilling certificate: `the-same-long-string.crt` +* a private key: `the-same-long-string.key` + +A hook is then run to enable the fragment, e.g. by linking it (it should not be +moved) into a `conf.d` directory (for Apache on Debian, `sites-enabled`). A +second hook is then run to check the configuration is valid, to avoid +accidental downtime, and then another to signal to the server to reload the +configuration. The server will now serve the generated certificate on a special +domain to prove you own the domain you're getting a certificate for. + +After the domain has been validated externally, hooks are run to disable the +configuration fragment, and again check and reload the configuration. + +Find your brand new certs in: + +``` +ls /etc/letsencrypt/live/ +``` + +To tailor this for your server setup, see all the `apache-` options in the list +below. Also note that the following substitutions are available for use in the +commands supplied to those options, and in any alternative template you +provide: + +* `{{{token}}}`: the token +* `{{{domain}}}`: the domain for which a certificate is being sought (beware of + this if using multiple domains per certificate) +* `{{{subject}}}`: the domain for which the generated challenge-fulfilling + certificate must be used (only available when generating it) +* `{{{cert}}}`: the path to the generated certificate: `apache-path/token.crt` +* `{{{privkey}}}`: the path to the generated private key: `apache-path/token.key` +* `{{{conf}}}`: the path to the generated config file: `apache-path/token.conf` +* `{{{bind}}}`: the value of the `apache-bind` option +* `{{{port}}}`: the value of the `apache-port` option +* `{{{webroot}}}`: the value of the `apache-webroot` option + ### Interactive (for debugging) The token (for all challenge types) and keyAuthorization (only for https-01) @@ -211,6 +273,34 @@ Options: --server [STRING] ACME Directory Resource URI. (Default is https://acme-v01.api.letsencrypt.org/directory)) + --apache BOOLEAN Obtain certs using Apache virtual hosts. + + --apache-path STRING Path in which to store files for Apache virtual hosts. + (Default is ~/letsencrypt/apache) + + --apache-bind [STRING] IP address to use for Apache virtual host. (Default is *) + (This is used in the default template.) + + --apache-port [NUMBER] Port to use for Apache virtual host. (Default is 443) + (This is used in the default template.) + + --apache-webroot STRING Webroot to use for Apache virtual host (e.g. an empty dir). + Nothing should actually be served from here. (Default is /var/www) + + --apache-template STRING Alternative template to use for Apache configuration file. + + --apache-enable STRING Command to run to enable the site in Apache. + (Default is `ln -s {{{conf}}} /etc/apache2/sites-enabled`) + + --apache-check STRING Command to run to check Apache configuration. + (Default is `apache2ctl configtest`) + + --apache-reload STRING Command to run to reload Apache. + (Default is `/etc/init.d/apache2 reload`) + + --apache-disable STRING Command to run to disable the site in Apache. + (Default is `rm /etc/apache2/sites-enabled/{{{token}}}.conf`) + --standalone [BOOLEAN] Obtain certs using a "standalone" webserver. (Default is true) --manual [BOOLEAN] Print the token and key to the screen and wait for you to hit enter, @@ -218,7 +308,7 @@ Options: --webroot BOOLEAN Obtain certs by placing files in a webroot directory. - --webroot-path STRING public_html / webroot path. + --webroot-path STRING public_html / webroot path. -h, --help Display help and usage details ``` diff --git a/bin/letsencrypt.js b/bin/letsencrypt.js index 28b2264..95dd2e2 100755 --- a/bin/letsencrypt.js +++ b/bin/letsencrypt.js @@ -10,8 +10,8 @@ cli.parse({ , duplicate: [ false, " Allow getting a certificate that duplicates an existing one", 'boolean', false ] , 'agree-tos': [ false, " Agree to the Let's Encrypt Subscriber Agreement", 'boolean', false ] , debug: [ false, " show traces and logs", 'boolean', false ] -, 'tls-sni-01-port': [ false, " Use TLS-SNI-01 challenge type with this port (only port 443 is valid with most production servers) (default: 443,5001)", 'string' ] -, 'http-01-port': [ false, " Use HTTP-01 challenge type with this port (only port 80 is valid with most production servers) (default: 80)", 'string' ] +, 'tls-sni-01-port': [ false, " Use TLS-SNI-01 challenge type with this port (only port 443 is valid with most production servers)", 'int' ] +, 'http-01-port': [ false, " Use HTTP-01 challenge type with this port (only port 80 is valid with most production servers) (default: 80)", 'int' ] , 'dns-01': [ false, " Use DNS-01 challange type", 'boolean', false ] , 'rsa-key-size': [ false, " Size (in bits) of the RSA key.", 'int', 2048 ] , 'cert-path': [ false, " Path to where new cert.pem is saved", 'string',':configDir/live/:hostname/cert.pem' ] @@ -25,6 +25,16 @@ cli.parse({ , manual: [ false, " Print the token and key to the screen and wait for you to hit enter, giving you time to copy it somewhere before continuing (default: false)", 'boolean', false ] , webroot: [ false, " Obtain certs by placing files in a webroot directory.", 'boolean', false ] , 'webroot-path': [ false, " public_html / webroot path.", 'string' ] +, apache: [ false, " Obtain certs using Apache virtual hosts.", 'boolean', false ] +, 'apache-path': [ false, " Path in which to store files for Apache virtual hosts.", 'string' ] +, 'apache-bind': [ false, " IP address to use for Apache virtual host.", 'string', "*" ] +, 'apache-port': [ false, " Port to use for Apache virtual host.", 'int', 443 ] +, 'apache-webroot': [ false, " Webroot to use for Apache virtual host (e.g. empty dir).", 'string' ] +, 'apache-template': [ false, " Alternative template to use for Apache configuration file.", 'string' ] +, 'apache-enable': [ false, " Command to run to enable the site in Apache.", 'string' ] +, 'apache-check': [ false, " Command to run to check Apache configuration.", 'string' ] +, 'apache-reload': [ false, " Command to run to reload Apache.", 'string' ] +, 'apache-disable': [ false, " Command to run to disable the site in Apache.", 'string' ] //, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-sni-01)", 'string', 'http-01,tls-sni-01'] , 'work-dir': [ false, "(ignored)", 'string', '~/letsencrypt/var/lib/' ] , 'logs-dir': [ false, "(ignored)", 'string', '~/letsencrypt/var/log/' ] diff --git a/index.js b/index.js index 1c40f1b..8a984e5 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ module.exports.run = function (args) { challengeType = 'dns-01'; args.webrootPath = ''; args.standalone = USE_DNS; - } else if (args.tlsSni01Port) { + } else if (args.tlsSni01Port || args.apache) { challengeType = 'tls-sni-01'; args.webrootPath = ''; } else /*if (args.http01Port)*/ { @@ -23,6 +23,19 @@ module.exports.run = function (args) { if (args.manual) { leChallenge = require('le-challenge-manual').create({}); } + else if (args.apache) { + leChallenge = require('le-challenge-apache').create({ + apachePath: args.apachePath + , apacheBind: args.apacheBind + , apachePort: args.apachePort + , apacheWebroot: args.apacheWebroot + , apacheTemplate: args.apacheTemplate + , apacheEnable: args.apacheEnable + , apacheCheck: args.apacheCheck + , apacheReload: args.apacheReload + , apacheDisable: args.apacheDisable + }); + } else if (args.webrootPath) { // webrootPath is all that really matters here // TODO rename le-challenge-fs to le-challenge-webroot diff --git a/package.json b/package.json index d23dd87..3ccac6a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "cli": "^0.11.1", "homedir": "^0.6.0", "le-acme-core": "^2.0.5", + "le-challenge-apache": "^2.0.1", "le-challenge-manual": "^2.0.0", "le-challenge-sni": "^2.0.0", "le-challenge-standalone": "^2.0.0",