update guide
This commit is contained in:
parent
b9bcdb5776
commit
fc04a0445c
|
@ -6,23 +6,48 @@ All options described for `Greenlock.create({...})` also apply to the Greenlock
|
||||||
|
|
||||||
# Overview of Major Differences
|
# Overview of Major Differences
|
||||||
|
|
||||||
## Greenlock JavaScript API greatly reduced
|
- Reduced API
|
||||||
|
- No code in the config
|
||||||
|
- (config is completely serializable)
|
||||||
|
- Manager callbacks replace `approveDomains`
|
||||||
|
- Greenlock Express does more, with less config
|
||||||
|
- cluster is supported out-of-the-box
|
||||||
|
- high-performance
|
||||||
|
- scalable
|
||||||
|
- ACME challenges are simplified
|
||||||
|
- init
|
||||||
|
- zones (dns-01)
|
||||||
|
- set
|
||||||
|
- get
|
||||||
|
- remove
|
||||||
|
- Store callbacks are simplified
|
||||||
|
- accounts
|
||||||
|
- checkKeypairs
|
||||||
|
- certificates
|
||||||
|
- checkKeypairs
|
||||||
|
- check
|
||||||
|
- set
|
||||||
|
|
||||||
|
# Greenlock JavaScript API greatly reduced
|
||||||
|
|
||||||
Whereas before there were many different methods with nuance differences,
|
Whereas before there were many different methods with nuance differences,
|
||||||
now there's just `create`, `get`, `renew`, and sometimes `add` ().
|
now there's just `create`, `get`, `renew`, and sometimes `add` ().
|
||||||
|
|
||||||
- Greenlock.create({ maintainerEmail, packageAgent, notify })
|
- Greenlock.create({ maintainerEmail, packageAgent, notify })
|
||||||
- Greenlock.get({ servername, wildname, duplicate, force })
|
- Greenlock.get({ servername, wildname, duplicate, force })
|
||||||
- (just a convenience wrapper around renew)
|
- (just a convenience wrapper around renew)
|
||||||
- Greenlock.renew({ subject, altnames, issuedBefore, expiresAfter })
|
- Greenlock.renew({ subject, altnames, issuedBefore, expiresAfter })
|
||||||
- (retrieves, issues, renews, all-in-one)
|
- (retrieves, issues, renews, all-in-one)
|
||||||
- _optional_ Greenlock.add({ subject, altnames, subscriberEmail })
|
- _optional_ Greenlock.add({ subject, altnames, subscriberEmail })
|
||||||
- (partially replaces `approveDomains`)
|
- (partially replaces `approveDomains`)
|
||||||
- `domains` was often ambiguous and confusing, it has been replaced by:
|
|
||||||
- `subject` refers to the subject of a certificate - the primary domain
|
Also, some disambiguation on terms:
|
||||||
- `altnames` refers to the domains in the SAN (Subject Alternative Names) section of the certificate
|
|
||||||
- `servername` refers to the TLS (SSL) SNI (Server Name Indication) request for a cetificate
|
- `domains` was often ambiguous and confusing, it has been replaced by:
|
||||||
- `wildname` refers to the wildcard version of the servername (ex: `www.example.com => *.example.com`)
|
- `subject` refers to the subject of a certificate - the primary domain
|
||||||
|
- `altnames` refers to the domains in the SAN (Subject Alternative Names) section of the certificate
|
||||||
|
- `servername` refers to the TLS (SSL) SNI (Server Name Indication) request for a cetificate
|
||||||
|
- `wildname` refers to the wildcard version of the servername (ex: `www.example.com => *.example.com`)
|
||||||
|
|
||||||
When you create an instance of Greenlock, you only supply package and maintainer info.
|
When you create an instance of Greenlock, you only supply package and maintainer info.
|
||||||
|
|
||||||
|
@ -35,21 +60,21 @@ var pkg = require('./package.json');
|
||||||
|
|
||||||
var Greenlock = require('greenlock');
|
var Greenlock = require('greenlock');
|
||||||
var greenlock = Greenlock.create({
|
var greenlock = Greenlock.create({
|
||||||
// used for the ACME client User-Agent string as per RFC 8555 and RFC 7231
|
// used for the ACME client User-Agent string as per RFC 8555 and RFC 7231
|
||||||
packageAgent: pkg.name + '/' + pkg.version,
|
packageAgent: pkg.name + '/' + pkg.version,
|
||||||
|
|
||||||
// used as the contact for critical bug and security notices
|
// used as the contact for critical bug and security notices
|
||||||
// should be the same as pkg.author.email
|
// should be the same as pkg.author.email
|
||||||
maintainerEmail: 'jon@example.com'
|
maintainerEmail: 'jon@example.com',
|
||||||
|
|
||||||
// used for logging background events and errors
|
// used for logging background events and errors
|
||||||
notify: function (ev, args) {
|
notify: function(ev, args) {
|
||||||
if ('error' === ev || 'warning' === ev) {
|
if ('error' === ev || 'warning' === ev) {
|
||||||
console.error(ev, args);
|
console.error(ev, args);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
console.info(ev, args);
|
||||||
}
|
}
|
||||||
console.info(ev, args);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -64,25 +89,25 @@ When you want to get a single certificate, you use `get`, which will:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
greenlock
|
greenlock
|
||||||
.get({ servername: 'www.example.com' })
|
.get({ servername: 'www.example.com' })
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// certificate is not on the approved list
|
// certificate is not on the approved list
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullchain = result.pems.cert + '\n' + result.pems.chain + '\n';
|
var fullchain = result.pems.cert + '\n' + result.pems.chain + '\n';
|
||||||
var privkey = result.pems.privkey;
|
var privkey = result.pems.privkey;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullchain: fullchain,
|
fullchain: fullchain,
|
||||||
privkey: privkey
|
privkey: privkey
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
// something went wrong in the renew process
|
// something went wrong in the renew process
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
By default **no certificates will be issued**. See the _manager_ section.
|
By default **no certificates will be issued**. See the _manager_ section.
|
||||||
|
@ -95,20 +120,20 @@ When you want to renew certificates, _en masse_, you use `renew`, which will:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
greenlock
|
greenlock
|
||||||
.renew({})
|
.renew({})
|
||||||
.then(function(results) {
|
.then(function(results) {
|
||||||
if (!result.length) {
|
if (!result.length) {
|
||||||
// no certificates found
|
// no certificates found
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [{ site, error }]
|
// [{ site, error }]
|
||||||
return results;
|
return results;
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
// an unexpected error, not related to renewal
|
// an unexpected error, not related to renewal
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
@ -129,28 +154,39 @@ with a few extra that are specific to Greenlock Express:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
require('@root/greenlock-express')
|
require('@root/greenlock-express')
|
||||||
.init(function() {
|
.init(function() {
|
||||||
return {
|
// This object will be passed to Greenlock.create()
|
||||||
cluster: false,
|
|
||||||
packageAgent: pkg.name + '/' + pkg.version,
|
var options = {
|
||||||
maintainerEmail: 'jon@example.com',
|
// some options, like cluster, are special to Greenlock Express
|
||||||
notify: function(ev, args) {
|
|
||||||
if ('error' === ev || 'warning' === ev) {
|
cluster: false,
|
||||||
console.error(ev, args);
|
|
||||||
return;
|
// The rest are the same as for Greenlock
|
||||||
}
|
|
||||||
console.info(ev, args);
|
packageAgent: pkg.name + '/' + pkg.version,
|
||||||
}
|
maintainerEmail: 'jon@example.com',
|
||||||
};
|
notify: function(ev, args) {
|
||||||
})
|
console.info(ev, args);
|
||||||
.serve(function(glx) {
|
}
|
||||||
glx.serveApp(function(req, res) {
|
};
|
||||||
res.end('Hello, Encrypted World!');
|
|
||||||
});
|
return options;
|
||||||
});
|
})
|
||||||
|
.serve(function(glx) {
|
||||||
|
// will start servers on port 80 and 443
|
||||||
|
|
||||||
|
glx.serveApp(function(req, res) {
|
||||||
|
res.end('Hello, Encrypted World!');
|
||||||
|
});
|
||||||
|
|
||||||
|
// you can get access to the raw server (i.e. for websockets)
|
||||||
|
|
||||||
|
glx.httpsServer(); // returns raw server object
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## _Manager_ replaces `approveDomains`
|
# _Manager_ replaces `approveDomains`
|
||||||
|
|
||||||
`approveDomains` was always a little confusing. Most people didn't need it.
|
`approveDomains` was always a little confusing. Most people didn't need it.
|
||||||
|
|
||||||
|
@ -167,56 +203,58 @@ The config file should look something like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"subscriberEmail": "jon@example.com",
|
"subscriberEmail": "jon@example.com",
|
||||||
"agreeToTerms": true,
|
"agreeToTerms": true,
|
||||||
"sites": {
|
"sites": {
|
||||||
"example.com": {
|
"example.com": {
|
||||||
"subject": "example.com",
|
"subject": "example.com",
|
||||||
"altnames": ["example.com", "www.example.com"]
|
"altnames": ["example.com", "www.example.com"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can specify a `acme-dns-01-*` or `acme-http-01-*` challenge plugin globally, or per-site. The same is true with `greenlock-store-*` plugins:
|
You can specify a `acme-dns-01-*` or `acme-http-01-*` challenge plugin globally, or per-site.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"subscriberEmail": "jon@example.com",
|
"subscriberEmail": "jon@example.com",
|
||||||
"agreeToTerms": true,
|
"agreeToTerms": true,
|
||||||
"sites": {
|
"sites": {
|
||||||
"example.com": {
|
"example.com": {
|
||||||
"subject": "example.com",
|
"subject": "example.com",
|
||||||
"altnames": ["example.com", "www.example.com"]
|
"altnames": ["example.com", "www.example.com"],
|
||||||
}
|
"challenges": {
|
||||||
},
|
"dns-01": {
|
||||||
"store": {
|
"module": "acme-dns-01-digitalocean",
|
||||||
"module": "greenlock-store-fs",
|
"token": "apikey-xxxxx"
|
||||||
"basePath": "~/.config/greenlock"
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The same is true with `greenlock-store-*` plugins:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"subscriberEmail": "jon@example.com",
|
"subscriberEmail": "jon@example.com",
|
||||||
"agreeToTerms": true,
|
"agreeToTerms": true,
|
||||||
"sites": {
|
"sites": {
|
||||||
"example.com": {
|
"example.com": {
|
||||||
"subject": "example.com",
|
"subject": "example.com",
|
||||||
"altnames": ["example.com", "www.example.com"],
|
"altnames": ["example.com", "www.example.com"]
|
||||||
"challenges": {
|
}
|
||||||
"dns-01": {
|
},
|
||||||
"module": "acme-dns-01-digitalocean",
|
"store": {
|
||||||
"token": "apikey-xxxxx"
|
"module": "greenlock-store-fs",
|
||||||
}
|
"basePath": "~/.config/greenlock"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customer Manager
|
### Customer Manager, the lazy way
|
||||||
|
|
||||||
At the very least you have to implement `find({ servername })`.
|
At the very least you have to implement `find({ servername })`.
|
||||||
|
|
||||||
|
@ -224,29 +262,29 @@ Since this is a very common use case, it's supported out of the box as part of t
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var greenlock = Greenlock.create({
|
var greenlock = Greenlock.create({
|
||||||
packageAgent: pkg.name + '/' + pkg.version,
|
packageAgent: pkg.name + '/' + pkg.version,
|
||||||
maintainerEmail: 'jon@example.com',
|
maintainerEmail: 'jon@example.com',
|
||||||
notify: notify,
|
notify: notify,
|
||||||
find: find
|
find: find
|
||||||
});
|
});
|
||||||
|
|
||||||
// In the simplest case you can ignore all incoming options
|
// In the simplest case you can ignore all incoming options
|
||||||
// and return a single site config in the same format as the config file
|
// and return a single site config in the same format as the config file
|
||||||
|
|
||||||
function find(options) {
|
function find(options) {
|
||||||
var servername = options.servername; // www.example.com
|
var servername = options.servername; // www.example.com
|
||||||
var wildname = options.wildname; // *.example.com
|
var wildname = options.wildname; // *.example.com
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
|
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify(ev, args) {
|
function notify(ev, args) {
|
||||||
if ('error' === ev || 'warning' === ev) {
|
if ('error' === ev || 'warning' === ev) {
|
||||||
console.error(ev, args);
|
console.error(ev, args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.info(ev, args);
|
console.info(ev, args);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -254,20 +292,101 @@ If you want to use wildcards or local domains, you must specify the `dns-01` cha
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function find(options) {
|
function find(options) {
|
||||||
var servername = options.servername; // www.example.com
|
var subject = options.subject;
|
||||||
var wildname = options.wildname; // *.example.com
|
// may include wildcard
|
||||||
return Promise.resolve([
|
var altnames = options.altnames;
|
||||||
{
|
var wildname = options.wildname; // *.example.com
|
||||||
subject: 'example.com',
|
return Promise.resolve([
|
||||||
altnames: ['example.com', 'www.example.com'],
|
{
|
||||||
challenges: {
|
subject: 'example.com',
|
||||||
'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' }
|
altnames: ['example.com', 'www.example.com'],
|
||||||
}
|
challenges: {
|
||||||
}
|
'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' }
|
||||||
]);
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Customer Manager, complete
|
||||||
|
|
||||||
|
To use a fully custom manager, you give the npm package name, or absolute path to the file to load
|
||||||
|
|
||||||
|
```js
|
||||||
|
Greenlock.create({
|
||||||
|
// Greenlock Options
|
||||||
|
maintainerEmail: 'jon@example.com',
|
||||||
|
packageAgent: 'my-package/v2.1.1',
|
||||||
|
notify: notify,
|
||||||
|
|
||||||
|
// file path or npm package name
|
||||||
|
manager: '/path/to/manager.js',
|
||||||
|
// options that get passed to the manager
|
||||||
|
myFooOption: 'whatever'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The manager itself is, again relatively simple:
|
||||||
|
|
||||||
|
- find(options)
|
||||||
|
- add(siteConfig)
|
||||||
|
- update(updates)
|
||||||
|
- remove(options)
|
||||||
|
- defaults(globalOptions) (as setter)
|
||||||
|
- defaults() => globalOptions (as getter)
|
||||||
|
|
||||||
|
`/path/to/manager.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports.create = function() {
|
||||||
|
var manager = {};
|
||||||
|
|
||||||
|
manager.find = async function({ subject, altnames, renewBefore }) {
|
||||||
|
if (subject) {
|
||||||
|
return getSiteConfigBySubject(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (altnames) {
|
||||||
|
// may include wildcards
|
||||||
|
return getSiteConfigByAnyAltname(altnames);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renewBefore) {
|
||||||
|
return getSiteConfigsWhereRenewAtIsLessThan(renewBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
manage.add = function({ subject, altnames }) {
|
||||||
|
return setSiteConfig(subject, { subject, altnames });
|
||||||
|
};
|
||||||
|
|
||||||
|
manage.update = function({ subject, renewAt }) {
|
||||||
|
// update the `renewAt` date of the site by `subject`
|
||||||
|
return mergSiteConfig(subject, { renewAt });
|
||||||
|
};
|
||||||
|
|
||||||
|
manage.remove = function({ subject, altname }) {
|
||||||
|
if (subject) {
|
||||||
|
return removeSiteConfig(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return removeFromSiteConfigAndResetRenewAtToZero(altname);
|
||||||
|
};
|
||||||
|
|
||||||
|
// set the global config
|
||||||
|
manage.defaults = function(options) {
|
||||||
|
if (!options) {
|
||||||
|
return getGlobalConfig();
|
||||||
|
}
|
||||||
|
return mergeGlobalConfig(options);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
# ACME Challenge Plugins
|
# ACME Challenge Plugins
|
||||||
|
|
||||||
The ACME challenge plugins are just a few simple callbacks:
|
The ACME challenge plugins are just a few simple callbacks:
|
||||||
|
@ -309,64 +428,69 @@ If you are just implenting in-house and are not going to publish a module, you c
|
||||||
`/path/to/project/my-hacky-store.js`:
|
`/path/to/project/my-hacky-store.js`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
module.exports.create = function(options) {
|
module.exports.create = function(options) {
|
||||||
// ex: /path/to/account.ecdsa.jwk.json
|
// ex: /path/to/account.ecdsa.jwk.json
|
||||||
var accountJwk = require(options.accountJwkPath);
|
var accountJwk = require(options.accountJwkPath);
|
||||||
// ex: /path/to/privkey.rsa.pem
|
// ex: /path/to/privkey.rsa.pem
|
||||||
var serverPem = fs.readFileSync(options.serverPemPath, 'ascii');
|
var serverPem = fs.readFileSync(options.serverPemPath, 'ascii');
|
||||||
var store = {};
|
var accounts = {};
|
||||||
|
var certificates = {};
|
||||||
|
var store = { accounts, certificates };
|
||||||
|
|
||||||
// bare essential account callbacks
|
// bare essential account callbacks
|
||||||
store.accounts = {
|
accounts.checkKeypair = function() {
|
||||||
checkKeypair: function() {
|
// ignore all options and just return a single, global keypair
|
||||||
// ignore all options and just return a single, global keypair
|
|
||||||
return Promise.resolve({
|
|
||||||
privateKeyJwk: accountJwk
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setKeypair: function() {
|
|
||||||
// this will never get called if checkKeypair always returns
|
|
||||||
return Promise.resolve({});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// bare essential cert and key callbacks
|
return Promise.resolve({
|
||||||
store.certificates = {
|
privateKeyJwk: accountJwk
|
||||||
checkKeypair: function() {
|
});
|
||||||
// ignore all options and just return a global server keypair
|
};
|
||||||
return {
|
accounts.setKeypair = function() {
|
||||||
privateKeyPem: serverPem
|
// this will never get called if checkKeypair always returns
|
||||||
};
|
|
||||||
},
|
|
||||||
setKeypair: function() {
|
|
||||||
// never gets called if checkKeypair always returns an existing key
|
|
||||||
return Promise.resolve(null);
|
|
||||||
},
|
|
||||||
check: function(args) {
|
|
||||||
var subject = args.subject;
|
|
||||||
// make a database call or whatever to get a certificate
|
|
||||||
return goGetCertBySubject(subject).then(function() {
|
|
||||||
return {
|
|
||||||
pems: {
|
|
||||||
chain: '<PEM>',
|
|
||||||
cert: '<PEM>'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
set: function(args) {
|
|
||||||
var subject = args.subject;
|
|
||||||
var cert = args.pems.cert;
|
|
||||||
var chain = args.pems.chain;
|
|
||||||
|
|
||||||
// make a database call or whatever to get a certificate
|
return Promise.resolve({});
|
||||||
return goSaveCert({
|
};
|
||||||
subject,
|
|
||||||
cert,
|
// bare essential cert and key callbacks
|
||||||
chain
|
certificates.checkKeypair = function() {
|
||||||
});
|
// ignore all options and just return a global server keypair
|
||||||
}
|
|
||||||
};
|
return {
|
||||||
|
privateKeyPem: serverPem
|
||||||
|
};
|
||||||
|
};
|
||||||
|
certificates.setKeypair = function() {
|
||||||
|
// never gets called if checkKeypair always returns an existing key
|
||||||
|
|
||||||
|
return Promise.resolve(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
certificates.check = function(args) {
|
||||||
|
var subject = args.subject;
|
||||||
|
// make a database call or whatever to get a certificate
|
||||||
|
return goGetCertBySubject(subject).then(function() {
|
||||||
|
return {
|
||||||
|
pems: {
|
||||||
|
chain: '<PEM>',
|
||||||
|
cert: '<PEM>'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
certificates.set = function(args) {
|
||||||
|
var subject = args.subject;
|
||||||
|
var cert = args.pems.cert;
|
||||||
|
var chain = args.pems.chain;
|
||||||
|
|
||||||
|
// make a database call or whatever to get a certificate
|
||||||
|
return goSaveCert({
|
||||||
|
subject,
|
||||||
|
cert,
|
||||||
|
chain
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -379,18 +503,18 @@ Then you could assign it as the default for all of your sites:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"subscriberEmail": "jon@example.com",
|
"subscriberEmail": "jon@example.com",
|
||||||
"agreeToTerms": true,
|
"agreeToTerms": true,
|
||||||
"sites": {
|
"sites": {
|
||||||
"example.com": {
|
"example.com": {
|
||||||
"subject": "example.com",
|
"subject": "example.com",
|
||||||
"altnames": ["example.com", "www.example.com"]
|
"altnames": ["example.com", "www.example.com"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"store": {
|
"store": {
|
||||||
"module": "/path/to/project/my-hacky-store.js",
|
"module": "/path/to/project/my-hacky-store.js",
|
||||||
"accountJwkPath": "/path/to/account.ecdsa.jwk.json",
|
"accountJwkPath": "/path/to/account.ecdsa.jwk.json",
|
||||||
"serverPemPath": "/path/to/privkey.rsa.pem"
|
"serverPemPath": "/path/to/privkey.rsa.pem"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
Loading…
Reference in New Issue