354 lines
13 KiB
Markdown
354 lines
13 KiB
Markdown
Greenlock™ for Express.js
|
|
=================
|
|
|
|
An easy-to-use ACME client for Free SSL and Automated HTTPS.
|
|
|
|
| Sponsored by [ppl](https://ppl.family) |
|
|
[Greenlock™](https://git.coolaj86.com/coolaj86/greenlock.js) for
|
|
[cli](https://git.coolaj86.com/coolaj86/greenlock-cli.js),
|
|
[cluster](https://git.coolaj86.com/coolaj86/greenlock-cluster.js),
|
|
**Express.js**,
|
|
[Koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js),
|
|
[hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js)
|
|
|
|
Features
|
|
========
|
|
|
|
- [x] Automatic HTTPS
|
|
- [x] Free SSL
|
|
- [x] Free Wildcard SSL
|
|
- [x] Multiple domain support (up to 100 altnames per SAN)
|
|
- [x] Dynamic Virtual Hosting (vhost)
|
|
- [x] Automatical renewal (10 to 14 days before expiration)
|
|
- [x] Great ACME support
|
|
- [x] ACME draft 11
|
|
- [x] Let's Encrypt v2
|
|
- [x] Let's Encrypt v1
|
|
- [x] Full node.js support
|
|
- [x] core https module
|
|
- [x] Express.js
|
|
- [x] [Koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js)
|
|
- [x] [hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js)
|
|
- [x] Extensible Plugin Support
|
|
- [x] AWS (S3, Route53)
|
|
- [x] Azure
|
|
- [x] CloudFlare
|
|
- [x] Digital Ocean
|
|
- [x] etcd
|
|
- [x] Redis
|
|
|
|
Install
|
|
=======
|
|
|
|
```bash
|
|
npm install --save greenlock-express@2.x
|
|
```
|
|
|
|
become a `communityMember`
|
|
==================
|
|
|
|
If you're the kind of person that likes the kinds of stuff that I do,
|
|
well, I want to do more of it and I'd like to get you involved.
|
|
|
|
When you set the `communityMember` option to `true` I save your
|
|
email and I'm able to inform you when there are mandatory updates
|
|
(such as with Let's Encrypt v2), notify you of important security issues,
|
|
give you early access to similar projects, and
|
|
get your feedback from time to time.
|
|
|
|
I'll also get a hash of domain names that receive and renew certificates,
|
|
which is a metric that has long interested me and may help me in getting
|
|
non-developers involved in this and future projects.
|
|
|
|
QuickStart
|
|
==========
|
|
<!--
|
|
[![Free SSL with Greenlock.js](https://i.imgur.com/Y8ix6Ts.png)](https://youtu.be/e8vaR4CEZ5s)
|
|
-->
|
|
|
|
### Screencast
|
|
|
|
Watch the QuickStart demonstration: https://youtu.be/e8vaR4CEZ5s
|
|
|
|
* [0:00](https://youtu.be/e8vaR4CEZ5s#t=0) - Intro
|
|
* [2:22](https://youtu.be/e8vaR4CEZ5s#t=142) - Demonstrating QuickStart Example
|
|
* [6:37](https://youtu.be/e8vaR4CEZ5s?t=397) - Troubleshooting / Gotchas
|
|
|
|
### Working Example Code
|
|
|
|
Here's a completely working example that will get you started.
|
|
|
|
```
|
|
git clone https://git.coolaj86.com/coolaj86/greenlock-express.js.git
|
|
pushd greenlock-express.js
|
|
npm install
|
|
popd
|
|
|
|
# edit 'email' and 'approveDomains' in
|
|
# greenlock-express.js/examples/simple.js
|
|
|
|
node greenlock-express.js/examples/simple.js
|
|
```
|
|
|
|
All you have to do is start the webserver and then visit it at its domain name.
|
|
|
|
`app.js`:
|
|
```javascript
|
|
'use strict';
|
|
|
|
require('greenlock-express').create({
|
|
|
|
// Let's Encrypt v2 is ACME draft 11
|
|
version: 'draft-11'
|
|
|
|
// You MUST change 'acme-staging-v02' to 'acme-v02' in production
|
|
, server: 'https://acme-staging-v02.api.letsencrypt.org/directory' // staging
|
|
|
|
// You MUST change this to a valid email address
|
|
, email: 'john.doe@example.com'
|
|
|
|
// You MUST NOT build clients that accept the ToS without asking the user
|
|
, agreeTos: true
|
|
|
|
// You MUST change these to valid domains
|
|
// NOTE: all domains will validated and listed on the certificate
|
|
, approveDomains: [ 'example.com', 'www.example.com' ]
|
|
|
|
// You MUST have access to write to directory where certs are saved
|
|
// ex: /home/foouser/acme/etc
|
|
, configDir: require('path').join(require('os').homedir(), 'acme', 'etc')
|
|
|
|
, app: require('express')().use('/', function (req, res) {
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
res.end('Hello, World!\n\n💚 🔒.js');
|
|
})
|
|
|
|
// Join the community to get notified of important updates and help me make greenlock better
|
|
, communityMember: true
|
|
|
|
//, debug: true
|
|
|
|
}).listen(80, 443);
|
|
```
|
|
|
|
### What if the example didn't work?
|
|
|
|
Double check the following:
|
|
|
|
* **Public Facing IP** for `http-01` challenges
|
|
* Are you running this *as* a public-facing webserver (good)? or localhost (bad)?
|
|
* Does `ifconfig` show a public address (good)? or a private one - 10.x, 192.168.x, etc (bad)?
|
|
* If you're on a non-public server, are you using the `dns-01` challenge?
|
|
* **correct ACME version**
|
|
* Let's Encrypt **v2** (ACME v2) must use `version: 'draft-11'`
|
|
* Let's Encrypt v1 must use `version: 'v01'`
|
|
* **valid email**
|
|
* You MUST set `email` to a **valid address**
|
|
* MX records must validate (`dig MX example.com` for `'john@example.com'`)
|
|
* **valid DNS records**
|
|
* You MUST set `approveDomains` to real domains
|
|
* Must have public DNS records (test with `dig +trace A example.com; dig +trace www.example.com` for `[ 'example.com', 'www.example.com' ]`)
|
|
* **write access**
|
|
* You MUST set `configDir` to a writeable location (test with `touch ~/acme/etc/tmp.tmp`)
|
|
* **port binding privileges**
|
|
* You MUST be able to bind to ports 80 and 443
|
|
* You can do this via `sudo` or [`setcap`](https://gist.github.com/firstdoit/6389682)
|
|
* **API limits**
|
|
* You MUST NOT exceed the API [**usage limits**](https://letsencrypt.org/docs/staging-environment/) per domain, certificate, IP address, etc
|
|
* **Red Lock, Untrusted**
|
|
* You MUST change the `server` value **in production**
|
|
* Shorten the 'acme-staging-v02' part of the server URL to 'acme-v02'
|
|
|
|
### Get it working in staging first!
|
|
|
|
There are a number of common problems related to system configuration -
|
|
firewalls, ports, permissions, etc - that you are likely to run up against
|
|
when using greenlock for your first time.
|
|
|
|
In order to avoid being blocked by hitting rate limits with bad requests,
|
|
you should always test against the `staging` server
|
|
(`https://acme-staging-v02.api.letsencrypt.org/directory`) first.
|
|
|
|
Plugins
|
|
=====
|
|
**IMPORTANT**: Community plugins may or may not be maintained and working. Please try with the defaults before switching to community plugins.
|
|
|
|
HTTP-01 Challenges
|
|
-----------
|
|
|
|
| | Plugin |
|
|
|:--------------:|:---------:|
|
|
| **Default (fs)** | [le-challenge-fs](https://git.coolaj86.com/coolaj86/le-challenge-fs.js.git) |
|
|
| AWS S3 | [llun/le-challenge-s3](https://github.com/llun/le-challenge-s3) |
|
|
| Digital Ocean | [bmv437/le-challenge-digitalocean](https://www.npmjs.com/package/le-challenge-digitalocean) |
|
|
| - | Build Your Own <br> [le-challenge-http-SPEC](https://git.coolaj86.com/coolaj86/le-challenge-manual.js.git) |
|
|
| Full List | Search [le-challenge-](https://www.npmjs.com/search?q=le-challenge-) on npm |
|
|
|
|
|
|
DNS-01 Challenges
|
|
-----------
|
|
|
|
| | Plugin |
|
|
|:--------------:|:---------:|
|
|
| **Manual (cli)** | [le-challenge-dns](https://git.coolaj86.com/coolaj86/le-challenge-dns.js.git) |
|
|
| AWS Route 53 | [thadeetrompetter/le-challenge-route53](https://github.com/thadeetrompetter/le-challenge-route53) |
|
|
| CloudFlare | [buschtoens/le-challenge-cloudflare](https://github.com/buschtoens/le-challenge-cloudflare) |
|
|
| CloudFlare | [llun/le-challenge-cloudflare](https://github.com/llun/le-challenge-cloudflare) |
|
|
| Digital Ocean | [bmv437/le-challenge-digitalocean](https://github.com/bmv437/le-challenge-digitalocean) |
|
|
| etcd | [ceecko/le-challenge-etcd](https://github.com/ceecko/le-challenge-etcd) |
|
|
| - | Build Your Own <br> [le-challenge-dns-SPEC](https://git.coolaj86.com/coolaj86/le-challenge-dns.js.git) |
|
|
| Full List | Search [le-challenge-](https://www.npmjs.com/search?q=le-challenge-) on npm |
|
|
|
|
Account & Certificate Storage
|
|
-----------
|
|
|
|
| | Plugin |
|
|
|:--------------:|:---------:|
|
|
| **Defaults (fs)** | [le-store-certbot](https://git.coolaj86.com/coolaj86/le-store-certbot.js.git) |
|
|
| AWS S3 | [paco3346/le-store-awss3](https://github.com/paco3346/le-store-awss3) |
|
|
| AWS S3 | [llun/le-store-s3](https://github.com/llun/le-store-s3) |
|
|
| Azure | [kolarcz/node-le-challenge-azure-storage](https://github.com/kolarcz/node-le-challenge-azure-storage) |
|
|
| json (fs) | [paulgrove/le-store-simple-fs](https://github.com/paulgrove/le-store-simple-fs)
|
|
| Redis | [digitalbazaar/le-store-redis](https://github.com/digitalbazaar/le-store-redis) |
|
|
| - | Build Your Own <br> [le-store-SPEC](https://git.coolaj86.com/coolaj86/le-store-SPEC.js.git) |
|
|
| Full List | Search [le-store-](https://www.npmjs.com/search?q=le-store-) on npm |
|
|
|
|
Bugs: Please report bugs with the community plugins to the appropriate owner first, then here if you don't get a response.
|
|
|
|
Usage
|
|
=====
|
|
The oversimplified example was the bait
|
|
(because everyone seems to want an example that fits in 3 lines, even if it's terribly bad practices),
|
|
now here's the switch.
|
|
|
|
We have another completely working example that will provides a little more to build off of.
|
|
|
|
```
|
|
git clone https://git.coolaj86.com/coolaj86/greenlock-express.js.git
|
|
pushd greenlock-express.js
|
|
npm install
|
|
popd
|
|
|
|
# replace 'fooCheckDb' in
|
|
# greenlock-express.js/examples/normal.js
|
|
|
|
node greenlock-express.js/examples/normal.js
|
|
```
|
|
|
|
It looks a little more like this:
|
|
|
|
`serve.js`:
|
|
```javascript
|
|
'use strict';
|
|
|
|
// returns an instance of greenlock.js with additional helper methods
|
|
var lex = require('greenlock-express').create({
|
|
// set to https://acme-v02.api.letsencrypt.org/directory in production
|
|
server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
|
|
, version: 'draft-11' // Let's Encrypt v2 (ACME v2)
|
|
|
|
// If you wish to replace the default plugins, you may do so here
|
|
//
|
|
, challenges: { 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) }
|
|
, store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
|
|
|
|
// You probably wouldn't need to replace the default sni handler
|
|
// See https://git.coolaj86.com/coolaj86/le-sni-auto if you think you do
|
|
//, sni: require('le-sni-auto').create({})
|
|
|
|
, approveDomains: approveDomains
|
|
|
|
// Join the community to get notified of important updates and help me make greenlock better
|
|
, communityMember: true
|
|
});
|
|
```
|
|
|
|
The Automatic Certificate Issuance is initiated via SNI (`httpsOptions.SNICallback`).
|
|
For security, domain validation MUST have an approval callback in *production*.
|
|
|
|
```javascript
|
|
function approveDomains(opts, certs, cb) {
|
|
// This is where you check your database and associated
|
|
// email addresses with domains and agreements and such
|
|
|
|
opts.communityMember = true;
|
|
|
|
// The domains being approved for the first time are listed in opts.domains
|
|
// Certs being renewed are listed in certs.altnames
|
|
if (certs) {
|
|
opts.domains = certs.altnames;
|
|
}
|
|
else {
|
|
opts.email = 'john.doe@example.com';
|
|
opts.agreeTos = true;
|
|
}
|
|
|
|
// NOTE: you can also change other options such as `challengeType` and `challenge`
|
|
// opts.challengeType = 'http-01';
|
|
// opts.challenge = require('le-challenge-fs').create({});
|
|
|
|
cb(null, { options: opts, certs: certs });
|
|
}
|
|
```
|
|
|
|
|
|
```javascript
|
|
// handles acme-challenge and redirects to https
|
|
require('http').createServer(lex.middleware(require('redirect-https')())).listen(80, function () {
|
|
console.log("Listening for ACME http-01 challenges on", this.address());
|
|
});
|
|
|
|
|
|
|
|
var app = require('express')();
|
|
app.use('/', function (req, res) {
|
|
res.end('Hello, World!');
|
|
});
|
|
|
|
// handles your app
|
|
require('https').createServer(lex.httpsOptions, lex.middleware(app)).listen(443, function () {
|
|
console.log("Listening for ACME tls-sni-01 challenges and serve app on", this.address());
|
|
});
|
|
```
|
|
|
|
**Security Warning**:
|
|
|
|
If you don't do proper checks in `approveDomains(opts, certs, cb)`
|
|
an attacker will spoof SNI packets with bad hostnames and that will
|
|
cause you to be rate-limited and or blocked from the ACME server.
|
|
|
|
|
|
API
|
|
===
|
|
|
|
This module is an elaborate ruse (to provide an oversimplified example and to nab some SEO).
|
|
|
|
The API is actually located at [greenlock.js options](https://git.coolaj86.com/coolaj86/greenlock.js)
|
|
(because all options are simply passed through to `greenlock.js` proper without modification).
|
|
|
|
The only "API" consists of two options, the rest is just a wrapper around `greenlock.js` to take LOC from 15 to 5:
|
|
|
|
* `opts.app` An express app in the format `function (req, res) { ... }` (no `next`).
|
|
* `lex.listen(plainPort, tlsPort)` Accepts port numbers (or arrays of port numbers) to listen on.
|
|
|
|
Brief overview of some simple options for `greenlock.js`:
|
|
|
|
* `opts.server` set to https://acme-v02.api.letsencrypt.org/directory in production
|
|
* `opts.version` set to `v01` for Let's Encrypt v1 or `draft-11` for Let's Encrypt v2 (mistakenly called ACME v2)
|
|
* `opts.email` The default email to use to accept agreements.
|
|
* `opts.agreeTos` When set to `true`, this always accepts the LetsEncrypt TOS. When a string it checks the agreement url first.
|
|
* `opts.communityMember` Join the community to get notified of important updates and help make greenlock better
|
|
* `opts.approveDomains` can be either of:
|
|
* An explicit array of allowed domains such as `[ 'example.com', 'www.example.com' ]`
|
|
* A callback `function (opts, certs, cb) { cb(null, { options: opts, certs: certs }); }` for setting `email`, `agreeTos`, `domains`, etc (as shown in usage example above)
|
|
* `opts.renewWithin` is the **maximum** number of days (in ms) before expiration to renew a certificate.
|
|
* `opts.renewBy` is the **minimum** number of days (in ms) before expiration to renew a certificate.
|
|
|
|
## Supported ACME versions
|
|
|
|
* Let's Encrypt v1 (aka v01)
|
|
* Let's Encrypt v2 (aka v02 or ACME draft 11)
|
|
* ACME draft 11 (ACME v2 is a misnomer)
|
|
* Wildcard domains (via dns-01 challenges)
|
|
* `*.example.com`
|