8.2 KiB
greenlock-store-memory
An in-memory reference implementation of a Certificate and Keypair storage strategy for Greenlock v2.7+ (and v3)
Usage
var greenlock = require('greenlock');
// This in-memory plugin has only one option: 'cache'.
// We could have It's used so that we can peek and poke at the store.
var cache = {};
var gl = greenlock.create({
store: require('greenlock-store-memory').create({ cache: cache })
, approveDomains: approveDomains
...
});
How to build your own module:
TL;DR: Just take a look the code here, and don't over think it.
Also, you have the flexibility to get really fancy. Don't! You probably don't need to (unless you already know that you do).
DON'T BE CLEVER. Do it the dumb way first.
In most cases you're just implementing dumb storage.
If all you do is JSON.stringify()
on set
(save) and JSON.parse()
after check
(get)
and just treat it as a blob with an ID, you'll do just fine. You can always optimize later.
Promises vs Thunks ("node callbacks") vs Synchronous returns: You can use whatever style you like best. Everything is promisified under the hood.
Whenever you have neither a result, nor an error, you must always return null (instead of 'undefined').
storage strategy vs approveDomains()
The most important thing to keep in mind: approveDomains()
is where all of the implementation-specific logic goes.
If you're writing a storage strategy (presumably why you're here), it's because you have logic in approveDomains()
that isn't supported by existing strategies. That makes it tempting to start thinking about things backwards, letting
your implementation-specific logic creep into your storage strategy. DON'T DO IT.
Keep in mind that, ultimately, it takes human decision / interaction / configuration to add, remove, or modify the collection of domains that are allowed, and how many / which domains are listed on each certificate - all of which is a completely separate process that lives outside of Greenlock (i.e uploading a site to a new folder).
The coupling between the method chosen for storage and the method chosen for approval is inherint, but keep it loose.
Lastly, it would be appropriate to include an example approveDomains()
with your implementation for reference.
0. approveDomains() is the kick off
approveDomains()
is called only when there is no certificate for a given domain in Greenlock's internal cache
and when that certificate is "renewable" (typically 15 days before expiration, which is configurable).
The user (perhaps you) will have checked in their database (or config file or file system) and retrieved relevant details (email associated with the domain, related domains that belong as altnames on the certificate, etc).
Those options will be available to all storage and challenge strategies. In fact, they can even change which strategy is used (i.e. some users using a Digital Ocean strategy for DNS challenges, others using Route53).
function approveDomains(opts) {
var info = userDb.getInfo(opts.domain);
if (!info) { throw new Error("ignoring junk request, bad domain"); }
opts.email = info.certificateOwner;
opts.subject = info.certificateSubject
opts.domains = info.certificateAltnames;
return opts; // or Promise.resolve(opts);
}
1. Implement accounts.setKeypair
First, you should implement accounts.setKeypair()
. Just treat it like dumb storage.
This only gets called after a new account has already been created successfully. That will only happen when a completely new certificate is going to be issued (not renewal), and there's no user account already associate with that set of domains.
store.accounts.setKeypair = function (opts) {
console.log('accounts.setKeypair:', opts);
var id = opts.account.id || opts.email || 'default';
var keypair = opts.keypair;
cache.accountKeypairs[id] = JSON.stringify({
privateKeyPem: keypair.privateKeyPem
, privateKeyJwk: keypair.privateKeyJwk
});
return null; // or Promise.resolve(null);
};
2. Implement accounts.checkKeypair
Whatever you did above, you just do the opposite instead. Tada!
store.accounts.checkKeypair = function (opts) {
console.log('accounts.checkKeypair:', opts);
var id = opts.account.id || opts.email || 'default';
var keyblob = cache.accountKeypairs[id];
if (!keyblob) { return null; }
return JSON.parse(keyblob);
};
3. (and 4.) Optionally save ACME account metadata
You should probably skip this and not worry about it.
However, if you have a special need for it, or if you want to shave off an ACME API call,
you can save the account kid
(a misnomer intended to mean "key id", but actually refers
to an arbitrary ACME URL, used to identify the account).
store.accounts.set = function (opts) {
console.log('accounts.set:', opts);
return null;
};
store.accounts.check = function (opts) {
var id = opts.account.id || opts.email || 'default';
console.log('accounts.check:', opts);
return null;
};
If you don't implement these the account key will be used to "recover" the kid
as necessary.
You don't have to worry though, it doesn't create a duplicate accounts or have any other negative
side affects other than an additional API call as needed.
5. Implement a method to save certificate keypairs
Each certificate is supposed to have a unique keypair, which must not be the same as the account keypair.
Again, just treat it like a blob in dumb storage and you'll be fine.
This is the same as accounts.setKeypair()
, but using a different idea.
You could even use the same data store in most cases because the IDs aren't likely to clash.
store.certificates.setKeypair = function (opts) {
console.log('certificates.setKeypair:', opts);
var id = opts.certificate.kid || opts.certificate.id || opts.subject;
var keypair = opts.keypair;
cache.certificateKeypairs[id] = JSON.stringify({
privateKeyPem: keypair.privateKeyPem
, privateKeyJwk: keypair.privateKeyJwk
});
return null;
};
6. Implement a method to get certificate keypairs
You know the drill. Same as accounts.checkKeypair()
, but a different ID.
This isn't called until after the certificate retrieval is successful.
Note: Every account must have a unique account key and account keys are not allowed to be used as certificate keys. However, you could use the same certificate key for all domains on a device (i.e. a server) or an account.
store.certificates.checkKeypair = function (opts) {
console.log('certificates.checkKeypair:', opts);
var id = opts.certificate.kid || opts.certificate.id || opts.subject;
var keyblob = cache.certificateKeypairs[id];
if (!keyblob) { return null; }
return JSON.parse(keyblob);
};
7. Implement a method to save certificates
Whenever the ACME process completes successfully, you get a shiny new certificate with all of the domains you requested.
It's a good idea to save them - otherwise you run the risk of running up your rate limit and getting blocked as your server restarts, respawns, auto-scales, etc.
store.certificates.set = function (opts) {
console.log('certificates.set:', opts);
var id = opts.certificate.id || opts.subject;
var pems = opts.pems;
cache.certificates[id] = JSON.stringify({
cert: pems.cert
, chain: pems.chain
, subject: pems.subject
, altnames: pems.altnames
, issuedAt: pems.issuedAt // a.k.a. NotBefore
, expiresAt: pems.expiresAt // a.k.a. NotAfter
});
return null;
};
Note that chain
is likely to be the same for all certificates issued by a service,
but there's no guarantee. The service may rotate which keys do the signing, for example.
8. Implement a method to get certificates
Lastly, you just need a way to fetch the result of all the work you've done.
store.certificates.check = function (opts) {
console.log('certificates.check:', opts);
var id = opts.certificate.id || opts.subject;
var certblob = cache.certificates[id];
if (!certblob) { return null; }
return JSON.parse(certblob);
};
Huzzah!
There you go - you basically just have 8 setter and getter functions that usually act as dumb storage, but that you can tweak with custom options if you need to.
Remember: Keep It Stupid-Simple
:D