Node.js configuration

For those of you using the https module in your node server, the official node documentation doesn’t have a whole lot on configuration. To secure your server properly, you must include privkey.pem, fullchain.pem, and chain.pem.

var https = require('https');
var fs = require('fs');
var options = {
     key: fs.readFileSync('/path/to/privkey.pem'),
     cert: fs.readFileSync('/path/to/fullchain.pem'),
     ca: fs.readFileSync('/path/to/chain.pem')
}
var server = https.createServer(options, handlerFunction);
server.listen(8080, '127.0.0.1');

Hope you guys don’t have as much trouble figuring this out as I did.

4 Likes

@hexadecatrienoic Thanks for posting this, I saw the node-acme stuff and was having a hard time wrapping my head around it. This looks more like familiar territory.

I have got a perhaps naiive follow up question: How exactly do you generate the certs, and how do you keep them renewed (every 90 days at most is the requirement, it seems). From the docs, it looks like using the manual method is the way to go. I’m not sure how to then go about telling a long-running node server to generate new certificates and start using the new ones at a particular frequency.

Any suggestions?

Looks like cert should point to fullchain.pem, not cert.pem for a correct configuration. ca is probably only used for client certificate authentication and irrelevant here.

Edit: It’s fixed in the initial post now.

@bguiz
I used a cron job that runs a script to update the certs. It stops the server from running while it updates the certs, which could likely be done much more cleanly, but it gets the job done.
The script:

#!/bin/sh
#update_certs.sh
systemctl stop httpserver.service
letsencrypt certonly -d <your_doman> --server https://acme-v01.api.letsencrypt.org/directory --renew-by-default --agree-tos
systemctl start httpserver.service

I’m using Arch Linux, and wrote a systemd unit file to run the server automatically. I will be looking into having the letsencrypt program place the file in my server path instead of stopping the server, but for now this does it.

I can confirm that both methods work when ca is set to chain.pem. We have started to delve into things I know work but can’t explain why.

For browsers, yes, because they’re liberal about that point. I’m sorry but it’s just incorrect, use any analysing tool and it will tell you that the chain is incomplete/not provided when using just cert.pem. I’m not a node.js user, I just debugged this together with a user on IRC having issue from just providing cert.pem.

@hexadecatrienoic Thanks for that. I’m running the command now, but it appears to be interactive - it asks me for an email address, etc. For crontab, I’m going to need a fully non-interactive one. I’m going to dig through the man pages to see what I can find (and I’ll report my findings back here).

@hexadecatrienoic Turns out that that was the only thing left to do: --email foo@bar.baz

Interestingly enough, you don’t seem to need --email after running interactively once.

You’re absolutely right. I’ve edited the first post. Thanks for pointing this out.

@hexadecatrienoic Yeah, it obviously must have persisted it somewhere on disk, and reuse it when --renew-by-default of the same domain.

@jhass Now I need to solve the ACME-challenge (see output below). When generating the first cert, this is not encountered, but subsequent runs appear to require this. I just came across this moments ago, so I’m going to research this now.

Failed authorization procedure. example.com (http-01): urn:acme:error:unauthorized :: The client lacks sufficient authorization :: Invalid response from http://example.com/.well-known/acme-challenge/SomeVeryLongHash [111.111.111.111]: 404

IMPORTANT NOTES:
 - The following 'urn:acme:error:unauthorized' errors were reported by
   the server:

   Domains: example.com
   Error: The client lacks sufficient authorization

@jhass I added --authenticator manual to my command and was able to get one renewal by serving up the appropriate file. However, subsequently the keys change, so it looks like this is unsuitable for a cron job.

Without “manual”, if I run the command I get the following:

Failed authorization procedure. example.com (tls-sni-01): urn:acme:error:unauthorized :: The client lacks sufficient authorization :: Correct zName not found for TLS SNI challenge. Found example.com

IMPORTANT NOTES:
 - The following 'urn:acme:error:unauthorized' errors were reported by
   the server:

   Domains: example.com
   Error: The client lacks sufficient authorization

Note that I have redacted my actual domain (using example.com instead), however, they do indeed match - I am running it from the same server that I am issuing the certificates from.

You have to use certonly with webroot or standalone mode if you want to automate the command and don’t run Apache or don’t want the client to modify its config. With webroot you’ll have to configure your webserver listening on port 80 to serve the file generated by the client at the given directory.

The official client intentionally rotates keys on renewal.

@jhass Thanks for that. I’ve been reading the docs, but it isn’t too clear exactly how to use --webroot or --standalone mode. I’ve pasted the help output into a gist, and highlighted the relevant parts here.

From what I gather, if not using certonly, letsencrypt-auto will place the rotated acme challenge keys in /var/www/public_html/.well-known/acme-challenge (some folder that Apache serves up automatically). I have set up my NodeJs server to behave in a similar way - It serves up files from a particular folder for HTTP GET /.well-known/acme-challenge/${someHash}. Is there a way I can get the authentication going once I have this?

1 Like

The folder is not served automatically by Apache, the client inserts configuration into it so that happens.

Yes.

@jhass I used --webroot --webroot-path as suggested in that link, unfortunately this did not put the ACME challenge file into the folder specified using --webroot-path, so I still fail the authorisation procedure.

It seems like it expects the challenge to pass first, before it will put stuff in there. Am I missing something?

My full command (redacted to example.com) was like this:

$ letsencrypt-auto \
  certonly \
  --webroot --webroot-path /path/served/by/nodejs/ \
  --domain example.com \
  --email example@example.com \
  --server https://acme-v01.api.letsencrypt.org/directory \
  --renew-by-default \
  --agree-tos

output:

Failed authorization procedure. example.com (http-01): urn:acme:error:unauthorized :: The client lacks sufficient authorization :: Invalid response from http:/example.com/.well-known/acme-challenge/someNewHash [111.111.111.111]: 500

The folder where the files are to be output appears to have created a subdirectory .well-known/acme-challenge, but that folder is empty - so something is happening, but not all the way

$ sudo ls -a /path/served/by/nodejs/.well-known/acme-challenge/ 
. ..

Am I still missing something?

You’re probably not serving the files right. The client removes the challenge after verification, no matter whether it succeeded or not. You can try manual mode and placing the challenge at the same path, giving you time to actually validate that your nodejs server is behaving correctly.

@jhass Ah! If only I had known this earlier! [quote=“jhass, post:17, topic:5175”] The client removes the challenge after verification, no matter whether it succeeded or not. [/quote]

@jhass Light at the end of the tunnel:

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at

You were right, there were file permissions errors, preventing my server from accessing the temporarily written file(s), and a running a chown command was the last piece of the puzzle.

Thanks very much for all the help with troubleshooting!

I’m getting “Empty reply from server” when I curl the server (despite responding in handlerFunction). What would happen if my certificates were not generated properly? That’s about all I can think of at this point. I tried OP’s exact code and many permutations and that’s all I ever get…

var options = {
  key: fs.readFileSync('privkey.pem'),
  cert: fs.readFileSync('fullchain.pem'),
  ca: fs.readFileSync('chain.pem')
}
var server = https.createServer(options, function(req, res) {
  res.writeHead(200);
  res.end('hello world\n');
});
server.listen(8080, '127.0.0.1');