greenlock-store-fs.js/README.md

3.4 KiB

greenlock-store-fs | A Root project

A keypair and certificate storage strategy for Greenlock v2.7+ (and v3). The (much simpler) successor to le-store-certbot.

Works with all ACME (Let's Encrypt) SSL certificate sytles:

  • single domains
  • multiple domains (SANs, AltNames)
  • wildcards
  • private / localhost domains

Usage

var greenlock = require('greenlock');
var gl = greenlock.create({
  configDir: '~/.config/acme'
, store: require('greenlock-store-fs')
, approveDomains: approveDomains
, ...
});

File System

The default file system layout mirrors that of le-store-certbot in order to make transitioning effortless, in most situations:

acme
├── accounts
│   └── acme-staging-v02.api.letsencrypt.org
│       └── directory
│           └── sites@example.com.json
└── live
    ├── example.com
    │   ├── bundle.pem
    │   ├── cert.pem
    │   ├── chain.pem
    │   ├── fullchain.pem
    │   └── privkey.pem
    └── www.example.com
        ├── bundle.pem
        ├── cert.pem
        ├── chain.pem
        ├── fullchain.pem
        └── privkey.pem

Wildcards & AltNames

Working with wildcards and multiple altnames requires greenlock >= v2.7 (or v3).

To do so you must return { subject: '...', altnames: ['...', ...] } within the approveDomains() callback.

subject refers to "the subject of the ssl certificate" as opposed to domain which indicates "the domain servername used in the current request". For single-domain certificates they're always the same, but for multiple-domain certificates subject must be the name no matter what domain is receiving a request. subject is used as part of the name of the file storage path where the certificate will be saved (or retrieved).

altnames should be the list of SubjectAlternativeNames (SANs) on the certificate. The subject and the first altname must be an exact match: subject === altnames[0].

Simple Example

function approveDomains(opts) {
  // Allow only example.com and *.example.com (such as foo.example.com)

  // foo.example.com => *.example.com
  var wild = '*.' + opts.domain.split('.').slice(1).join('.');

  if ('example.com' !== opts.domain && '*.example.com' !== wild) {
    cb(new Error(opts.domain + " is not allowed"));
  }

  var result = { subject: 'example.com', altnames: [ 'example.com', '*.example.com' ] };
  return Promise.resolve(result);
}

Realistic Example

function approveDomains(opts, certs, cb) {
  var related = getRelated(opts.domain);
  if (!related) { cb(new Error(opts.domain + " is not allowed")); };

  opts.subject = related.subject;
  opts.domains = related.domains;

  cb({ options: opts, certs: certs });
}
function getRelated(domain) {
  var related;
  var wild = '*.' + domain.split('.').slice(1).join('.');
  if (Object.keys(allAllowedDomains).some(function (k) {
    return allAllowedDomains[k].some(function (name) {
      if (domain === name || wild === name) {
        related = { subject: k, altnames: allAllowedDomains[k] };
        return true;
      }
    });
  })) {
    return related;
  }
}
var allAllowedDomains = {
  'example.com': ['example.com', '*.example.com']
, 'example.net': ['example.net', '*.example.net']
}