Is it Possible to Dynamically Return an SSL Certificate in NodeJS?
Asked Answered
A

3

42

I want to dynamically return an ssl certificate info in my NodeJS application. I have two domain names linked to the same node application. I only see that the ssl settings can be specified when the server is created. Is it possible to dynamically return ssl certificates based on the requested url?

Otherwise, if I must instead create a second sever instance on another port, will I be able to transparently pipe each request to the original port? Can I make it appear like it's not running on a second port?

Thanks, Jeff

Alannaalano answered 31/8, 2012 at 17:0 Comment(2)
I would like an answer to this also. I'm planning to build a Node.js app that can host multiple domains with a SSL cert for each. Would be useful if we can store the SSL cert info in the DB. So once we detect the domain they are coming from, we can serve their site theme and content. I know Node.js has a way to define a SSL cert for when it starts but don't know of a way to do it dynamically based on the domain they are on.Landon
not sure, but wouldn't github.com/nodejitsu/node-http-proxy be helpful?Adz
M
65

Yes, it is possible to do it with one server. But the caveat is that it works on clients that support SNI - which is most modern browsers.

This is how you do it:


    //function to pick out the key + certs dynamically based on the domain name
    function getSecureContext (domain) {
        return crypto.createCredentials({
            key:  fs.readFileSync('/path/to/domain.key'),
            cert: fs.readFileSync('/path/to/domain.crt'),
            ca: [
                fs.readFileSync('/path/to/CA_cert_1.crt'), 
                fs.readFileSync('/path/to/CA_cert_2.crt'), 
                // <include all CA certs that you have to> ... 
            ]
          }).context;
    }

    //read them into memory
    var secureContext = {
        'domain1': getSecureContext('domain1'),
        'domain2': getSecureContext('domain2'),
        // etc
    }

    //provide a SNICallback when you create the options for the https server
    var options = {
        SNICallback: function (domain) {
            return secureContext[domain];
        }, // SNICallback is passed the domain name, see NodeJS docs on TLS
        cert: fs.readFileSync('/path/to/server.crt'),
        key: fs.readFileSync('/path/to/server.key'),                
        }
    }

    //create your https server
    var server = require('https').createServer(options, [requestListener]);
    //using Express
    var server = require('https').createServer(options, require('express')());
    server.listen(<someport>);

This works because the options for https is similar to tls.createServer(). Make sure you include all required CA intermediate and root certificates in the crypto.createCredentials call. Also if you have a CA bundle, split them up into multiple single crt files before using them as 'ca' accepts an array of certificates.

Montespan answered 29/11, 2013 at 12:33 Comment(14)
Nice! Will each SSL cert need to have a dedicated IP? I want to host multiple domains with SSL on the same server. Figured when they buy their site, we'll automatically buy a SSL cert and domain for them using an API. If I could run multiple domains on the same server with the same IP, that would be great but have a feeling each cert will need a dedicated IP but not too sure.Landon
and I would want to be available to dynamically remove/add websites. Building a SaaS app or want to at least and this part would be something we need to figure out early. After reading that wikipedia page it looks like we don't need a dedicated IP. So that's a plus but is getSecureContext called at request time or when the server starts up? Because getSecureContext could then look up in a database every time the site gets a request for the SSL cert. hmm. Trying to think about how this should work.Landon
and wonder what it will do on older browsers that don't support it. Give them certificate errors I assume?Landon
Also wondering how you would make this work with Express.JS. Only Node.js framework I ever used.Landon
All SSL certs can stay on the same IP, it should'nt matter as log as the right one gets used. Since you'll probably have one-to-one mapping from domain to certificate (I have not used multi-domain certificates), you can always load all involved websites to memory or like you say, read from disk and update in-memory variable as you go. About older/any browser, I guess you can test with self-signed certs and check if the SNICallback function is triggered. As the NodeJS docs mention, this function is called for clients that support SNI. See edit for using Express.Montespan
So I guess when a domain is added or removed, I can edit the secureContext option? Or look up in a MySQL DB in the SNICallback: function (domain) call back? Guess it's called everytime a page from a domain pointed to the server is loaded? Let me know if I'm correct please. But major help so far! Giving you the +50 bounty.Landon
It is called every time your server is sent a request from an SNI-supported client. So you can decide during each request how and what (a context based on domain name in this case) to return from that callback.Montespan
Okay. In SNICallback, what do we do if we don't have that domain in our system? What would we return? Say they had an A record pointing at our server but the domain wasn't in the database? Also what should be returned if DB look up failed? I was thinking a node would download the certs from a central internal server and cache them. Each node will be under a load balancer at the TCP level. And also wondering if the browser doesn't support it, if it's possible to redirect them to a page/domain mentioning they should upgrade their browser.Landon
Anyone have an answer for the last question? Know it's been a while. So just wondering.Landon
Update: tls.createServer changed the usage of SNICallback (idk since when). SNICallback now comes with 2 parameters, domain & callback. Sample usage should match this to work. "SNICallback": function (domain, cb) { cb(null, getSecureContext(domain)); },Crease
The above code works great, but how would I redirect http to https? When I visit a https page, then the connection is secure, but in my node app, the function in http.createServer gets invoked instead of the https.createServer.Msg
Why do you specify a certificate at all in the options? Doesn't the SNICallback get called on every request?Abaca
@Crease OR: function (domain,cb) {var ctx=getSecureContext(domain);return cb ? cb(null,ctx) : ctx;} for backward compatiblityHosmer
@PeterKazazes the key and cert are required for tls.createServer(), probably in case the SNICallback doens't work.Landseer
L
31

crypto.createCredentials() is deprecated, so use tls.createSecureContext() instead.

tls.createServer() must have key and cert in the options, because they are required in the manual. Perhaps tls.createServer() uses these parameters as defaults in case SNICallback is not supported.

var secureContext = {
    'mydomain.com': tls.createSecureContext({
        key: fs.readFileSync('../path_to_key1.pem', 'utf8'),
        cert: fs.readFileSync('../path_to_cert1.crt', 'utf8'),
        ca: fs.readFileSync('../path_to_certificate_authority_bundle.ca-bundle1', 'utf8'), // this ca property is optional
    }),
    'myotherdomain.com': tls.createSecureContext({
        key: fs.readFileSync('../path_to_key2.pem', 'utf8'),
        cert: fs.readFileSync('../path_to_cert2.crt', 'utf8'),
        ca: fs.readFileSync('../path_to_certificate_authority_bundle.ca-bundle2', 'utf8'), // this ca property is optional
    }),
}
try {
    var options = {
        SNICallback: function (domain, cb) {
            if (secureContext[domain]) {
                if (cb) {
                    cb(null, secureContext[domain]);
                } else {
                    // compatibility for older versions of node
                    return secureContext[domain]; 
                }
            } else {
                throw new Error('No keys/certificates for domain requested');
            }
        },
       // must list a default key and cert because required by tls.createServer()
        key: fs.readFileSync('../path_to_key.pem'), 
        cert: fs.readFileSync('../path_to_cert.crt'), 
    }
    https.createServer(options, function (req, res) {
        res.end('Your dynamic SSL server worked!')
        // Here you can put proxy server routing here to send the request 
        // to the application of your choosing, running on another port.
        // node-http-proxy is a great npm package for this
    }).listen(443);
} catch (err){
    console.error(err.message);
    console.error(err.stack);
}

Inside the server you can use nodejs package http-proxy to route your https request to your various applications.

Landseer answered 10/8, 2016 at 0:58 Comment(2)
throwing an error if there are no keys/certs for the domain requested isn't acceptable for production because it means that the server croAKs. what's a better way for handling this?Ax
Node 12.16.1: running https.createServer() with just the SNICallback property set works for me.Misdate
H
3

Someone who had opened up an issue in greenlock-express.js and referenced this post, so I'll include the way to do this with Greenlock for Let's Encrypt here as well:

Use Greenlock.js for Dynamic SSL Certificates

Greenlock does exactly what you need, but bakes in security and convenience.

  • Dynamic loading of tls certificates using a structured directory path
  • Automated SSL certificate issuance and renewal via Let's Encrypt v2
  • Protects against SNI and Host attacks, and domain fronting.

Install

npm install --save greenlock-express

Use Let's Encrypt via Greenlock

require("greenlock-express")
    .init(function getConfig() {
        return { package: require("./package.json") };
    })
    .serve(httpsWorker);

function httpsWorker(server) {
    // Works with any Node app (Express, etc)
    var app = require("./my-express-app.js");

    // See, all normal stuff here
    app.get("/hello", function(req, res) {
        res.end("Hello, Encrypted World!");
    });

    // Serves on 80 and 443
    // Get's SSL certificates magically!
    server.serveApp(app);
}

Documentation

The video section specifically pertaining to configuration for dynamic domain loading: 2:26 Greenlock for node.js Part 2: Configuration

Important Side Note: Security Considerations

Greenlock already mitigates these security issues, but if you're implementing by hand there are some things you should know to stay safe:

In particular, it's really important to be aware that you can make yourself vulnerable to SQL injection and/or timing attacks when you are dynamically loading ssl certs with code you write yourself.

Though you expect valid bytes like example.com to come through node's tls.SNICallback(sni, cb) and req.socket.servername, you can actually get a visit from Robert'); DROP TABLE Students; (or little Bobby Tables as we like to call him).

If you're interested in seeing how that exploit could work, I've documented it here in Greenlock for node.js Part 3: Security Concerns and https://github.com/nodejs/node/issues/22389

You can also become vulnerable to Domain Fronting, which is a fairly low-risk attack/side-channel, but is important to know and understand.

Haynes answered 23/8, 2018 at 5:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.