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 Serverapi.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
Add
app.mylocaldomain.com
andapp.mylocaldomain.com
to/etc/hosts
... 127.0.0.1 api.mylocaldomain.com 127.0.0.1 app.mylocaldomain.com ...
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
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
Create App
cert.key
andcert.pem
files:openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout app/cert.key -out app/cert.pem -config app/req.cnf -sha256
Create API
cert.key
andcert.pem
files:openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout api/cert.key -out api/cert.pem -config api/req.cnf -sha256
Load both certificates to Mac Keychain Access and approve both to be trusted
This is the working result with both certificates approved in keychain:
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') }, ... }
Start the application with
sudo yarn start
(webpack-dev-server
in npm scripts) to allow using443
portThis is the working result:
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 ...
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); } }); ...
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:
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:
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.