0

The question deals with using self-signed certificates in local full-stack (in this case React application and Node.js FE server) development.

Note that the question itself is probably not as complex as the length of it would suggest. Simply, I needed to describe the entire setup to avoid dealing with non-relevant trivial suggestions. Also, when/if the question is answered, it should serve as a nice starting point for such endeavours.

Reason

Many features, including but not limited to shared signed cookies, http2, etc..., work only over HTTPS or require sibling/parent domain setup, which brings forward the need to create a certificate and run both Webpack Dev Server and Nodejs FE server over HTTPS (locally) on explict domains and not on localhost.

Note that the problem is specific to the local development, as it is properly configured with non-self-signed certificates and over HTTPS in production.

Assumption

While it is possible to create a "normal" certificate using something like Let's Encrypt, it is not always possible or desirable (company policies or some such).

For this reason, and for sheer academic purposes, we choose to use self-signed certificate.

Setup

OS: Mac Catalina 10.15.2 Node: 13.8

There are two sub-domains:

  • app.mylocaldomain.com which hosts the React application via Webpack Dev Server
  • api.mylocaldomain.com which hosts the FE API server via Node.js + Express

Need

Be able to make an HTTPS AJAX request from https://app.mylocaldomain.com to https://api.mylocaldomain.com/some-route

Steps

  1. Add app.mylocaldomain.com and app.mylocaldomain.com to /etc/hosts

    ...
    127.0.0.1   api.mylocaldomain.com
    127.0.0.1   app.mylocaldomain.com
    ...
    
  2. Create api/req.cnf file

    [req]
    distinguished_name = req_distinguished_name
    x509_extensions = v3_req
    prompt = no
    [req_distinguished_name]
    C = US
    ST = State
    L = Location
    O = myorg
    OU = myunit 
    CN = api.mylocaldomain.com
    [v3_req]
    keyUsage = critical, digitalSignature, keyAgreement
    extendedKeyUsage = serverAuth
    subjectAltName = @alt_names
    [alt_names]
    DNS.1 = api.mylocaldomain.com
    
  3. Create app/req.cnf file

    [req]
    distinguished_name = req_distinguished_name
    x509_extensions = v3_req
    prompt = no
    [req_distinguished_name]
    C = US
    ST = State
    L = Location
    O = myorg
    OU = myunit 
    CN = app.mylocaldomain.com
    [v3_req]
    keyUsage = critical, digitalSignature, keyAgreement
    extendedKeyUsage = serverAuth
    subjectAltName = @alt_names
    [alt_names]
    DNS.1 = app.mylocaldomain.com
    
  4. Create App cert.key and cert.pem files:

    openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout app/cert.key -out app/cert.pem -config app/req.cnf -sha256
    
  5. Create API cert.key and cert.pem files:

    openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout api/cert.key -out api/cert.pem -config api/req.cnf -sha256
    
  6. Load both certificates to Mac Keychain Access and approve both to be trusted

    This is the working result with both certificates approved in keychain:

    enter image description here

  7. Configure the application Webpack Dev Server to start over HTTPS

    devServer: {
        ...
        host: 'app.mylocaldomain.com',
        port: 443,
        https: {
            key: FS.readFileSync('/path/to/app/cert.key'),
            cert: FS.readFileSync('/path/to/app/cert.pem')
        },
        ...
    }
    
  8. Start the application with sudo yarn start (webpack-dev-server in npm scripts) to allow using 443 port

    This is the working result:

    enter image description here

    where the application is properly loaded over HTTPS.

    Note that omitting sudo causes the following (or similar, depending on your Webpack Dev Server setup):

    ...
    Error: listen EACCES: permission denied 127.0.0.1:443
    ...
    
  9. Configure the FE server to start over HTTPS

    ...
    const app = express(...)
    ...
    const key = fs.readFileSync('/path/to/api/cert.key');
    const cert = fs.readFileSync('/path/to/api/cert.pem');
    
    https.createServer({key, cert}, app).listen(PORT, error => {
        if (!error) {
            console.info(`Listening on port: ${PORT}`);
        } else {
            console.error(error);
        }
    });
    ...
    
  10. Start the FE API server using sudo yarn start --open --public api.mylocaldomain.com --port 443 (node lib/ in npm scripts)

    yarn run v1.22.0
    $ node lib/ --public api.mylocaldomain.com
    Listening on port: 5000   
    

Problem

The above setup should have been enough, however, invoking an AJAX request to https://api.mylocaldomain.com/status (yes, the route exists) causes the following (abridged for clarity) error in Chrome console:

GET https://api.mylocaldomain.com/status
net::ERR_CERT_COMMON_NAME_INVALID

If you were to access https://api.mylocaldomain.com/status directly in Chrome, you'd get the following:

enter image description here

This would suggest that the certificates for App and API got somehow mixed, but I ran the above steps in various permutations multiple times and fail to see how that would happen. Perhaps /etc/hosts mapping mucks this up? Would make sense, since both app. and api. are mapped into 127.0.0.1 creating the mixup. However, I am conflicted as to how resolve this.

Indeed, if inspected, it seems that Chrome (at least) loads App certificate when API is accessed. Why?

Following the link above leads to:

enter image description here

Invoking curl -XGET https://api.mylocaldomain.com/status causes the following error:

>>> curl -XGET https://api.mylocaldomain.com/status
curl: (60) SSL: no alternative certificate subject name matches target host name 'api.mylocaldomain.com'
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

which is odd, as api.mylocaldomain.com is explicitly defined in the req.cnf for the API above.


Suggestions?

I assume that this is something extremely minuscule and I will feel embarrassed for having missed it, but after multiple passes and re-tries, it may have entered into my blind spot(s).

Appendix A.

Creating a multi-domain *.mylocaldomain.com certificate doesn't work either, with the error being 'invalid common name' or Invalid Host header depending on various setting permutations.

ZenMaster
  • 12,363
  • 5
  • 36
  • 59
  • 1
    `Many features, including but not limited to signed cookies` You might not know this and might simplify things for you. `localhost` over port 80 is classed as secure origin too, so for things that require SSL will work on localhost over HTTP too without even having to create a self signed cert. – Keith Feb 24 '20 at 14:25
  • @Keith I am aware of this, however, in order to do shared signed cookies (which was the entire point) you need to have HTTPS and sibling domains -- which can't be done over `localhost`, as port is a part of 3rd party domain definition, thus not allowing you to have app at `localhost:3000` and API at `localhost:5000` and having the cookies be shared properly. Finally, HTTP2 doesn't work without HTTPS in Webpack AFAIK (but that's for our future uses and is not central to this). However -- amended the OP. thank you. – ZenMaster Feb 24 '20 at 14:31
  • 1
    Ok, we do similar stuff. We have single wildcard domain, this is a proper domain so no need to mess about with host files / but you still can if say you wanted to use firewall IP's. We just then create lots of `A` records like `app1` ,`app2` etc,. or even specific ones if logical. If your worried about cost of getting a wildcard domain is an issue, letsencrypt now supports wildcards. Finally stick this behind a reverse proxy. – Keith Feb 24 '20 at 14:45
  • 1
    In the long run this is much better than messing with self signed certs, because even when you do get this working, it still has issues. eg. in Chrome ajax / fetch responses will silently start failing because Chrome periodically wants you to re-confirm the self signed cert. – Keith Feb 24 '20 at 14:46
  • @Keith you don't need to reconfirm them AFAIK since they are in the keychain. On Windows -- 100% you need to. You are probably right, but it has been started in this direction and I'd like to see it through. – ZenMaster Feb 24 '20 at 14:50

0 Answers0