devcert - Development SSL made easy
So, running a local HTTPS server usually sucks. There's a range of
approaches, each with their own tradeoff. The common one, using self-signed
certificates, means having to ignore scary browser warnings for each project.
devcert makes the process easy. Want a private key and certificate file to
use with your server? Just ask:
let ssl = await devcert.certificateFor('my-app.test');
https.createServer(ssl, app).listen(3000);
Now open https://my-app.test:3000 and voila - your page loads with no scary
warnings or hoops to jump through.
Certificates are cached by name, so two calls for
certificateFor('foo')
will return the same key and certificate.
Options
When it installs or upgrades, devcert creates a self-signed certificate
authority (CA) which it uses to sign all certificates it creates. It will try
to register this CA with OS keychains in OSX, Linux, and Windows. However,
some HTTP clients (such as Firefox and NodeJS itself) use their own trusted
certificate list instead of the operating system's keychain. The getCaPath
and getCaBuffer
options make the CA available in the certificateFor()
return object itself, so that these programs can choose whether to trust it.
getCaPath
Set this option to true
and the returned object will inlude a caPath
property, set to the file path of the certificate authority file. Use this
path to add the certificate to local trust stores which accept paths as
arguments, such as NodeJS's builtin environment variable
NODE_EXTRA_CA_CERTS
..
getCaBuffer
Set this option to true
and the returned object will inlude a ca
property, set to the UTF-8-encoded contents of the certificate authority
file. Use this path to add the certificate to local trust stores which don't
use OS settings, lke the examples mentioned above.
skipHostsFile
If you supply a custom domain name (i.e. any domain other than localhost
)
when requesting a certificate from devcert, it will attempt to modify your
system to redirect requests for that domain to your local machine (rather
than to the real domain). It does this by modifying your /etc/hosts
file.
If you pass in the skipHostsFile
option, devcert will skip this step. This
means that if you ask for certificates for my-app.test
(for example), and
don't have some other DNS redirect method in place, that you won't be able to
access your app at https://my-app.test
because your computer wouldn't know
that my-app.test
should resolve your local machine.
Keep in mind that SSL certificates are issued for domains, so if you ask
for a certificate for my-app.test
, and don't have any kind of DNS redirect
in place (/etc/hosts
or otherwise), trying to hit localhost
won't work,
even if the app you intended to serve via my-app.test
is running on your
local machine (since the SSL certificate won't say localhost
).
skipCertutil
This option will tell devcert to avoid installing certutil
tooling.
certutil
is a tooling package used to automated the installation of SSL
certificates in certain circumstances; specifically, Firefox (for every OS)
and Chrome (on Linux only).
Normally, devcert will attempt to install certutil
if it's need and not
already present on your system. If don't want devcert to install this
package, pass skipCertutil: true
.
If you decide to skipCertutil
, the initial setup process for devcert
changes in these two scenarios:
-
Firefox on all platforms: Thankully, Firefox makes this easy. There's a
point-and-click wizard for importing and trusting a certificate, so if you
specify skipCertutil: true
, devcert will instead automatically open Firefox
and kick off this wizard for you. Simply follow the prompts to trust the
certificate. Reminder: you'll only need to do this once per machine
-
Chrome on Linux: Unfortunately, it appears that the only way to get
Chrome to trust an SSL certificate on Linux is via the certutil
tooling -
there is no manual process for it. Thus, if you are using Chrome on Linux, do
not supply skipCertuil: true
. If you do, devcert certificates will not
be trusted by Chrome.
The certutil
tooling is installed in OS-specific ways:
- Mac:
brew install nss
- Linux:
apt install libnss3-tools
- Windows: N/A (there is no easy, hands-off way to install certutil on Windows,
so devcert will simply fallback to the wizard approach for Firefox outlined
above)
Multiple domains (SAN)
If you are developing a multi-tenant app or have many apps locally, you can generate a security
certificate using devcert
to also use the Subject Alternative Name
extension, just pass an array of domains instead.
let ssl = await devcert.certificateFor([
'localhost',
'local.api.example.com',
'local.example.com',
'local.auth.example.com'
]);
https.createServer(ssl, app).listen(3000);
Docker and local development
If you are developing with Docker, one option is to install devcert
into a base folder in your home directory and
generate certificates for all of your local Docker projects. See comments and caveats in this issue.
While not elegant, you only really need to do this as often as you add new domains locally, which is probably not very often.
The general script would look something like:
const fs = require('fs');
const devcert = require('devcert');
devcert.certificateFor([
'localhost',
'local.api.example.com',
'local.example.com',
'local.auth.example.com'
])
.then(({key, cert}) => {
fs.writeFileSync('./certs/tls.key', key);
fs.writeFileSync('./certs/tls.cert', cert);
})
.catch(console.error);
An easy way to use the files generated from above script is to copy the ~/devcert-util/certs
folder into your Docker projects:
# local-docker-project-root/
🗀 certs/
🗎 tls.key
🗎 tls.cert
And add this line to your .gitignore
:
certs/
These two files can now easily be used by any project, be it Node.js or something else.
In Node, within Docker, simply load the copied certificate files into your https server:
const fs = require('fs');
const Express = require('express');
const app = new Express();
https
.createServer({
key: fs.readFileSync('./certs/tls.key'),
cert: fs.readFileSync('./certs/tls.cert')
}, app)
.listen(3000);
Also works with webpack dev server or similar technologies:
const fs = require('fs');
module.exports = {
devServer: {
contentBase: join(__dirname, 'dist'),
host: '0.0.0.0',
public: 'local.api.example.com',
port: 3000,
publicPath: '/',
https: {
key: fs.readFileSync('./certs/tls.key'),
cert: fs.readFileSync('./certs/tls.cert')
}
}
};
How it works
When you ask for a development certificate, devcert will first check to see
if it has run on this machine before. If not, it will create a root
certificate authority and add it to your OS and various browser trust stores.
You'll likely see password prompts from your OS at this point to authorize
the new root CA.
Since your machine now trusts this root CA, it will trust any certificates
signed by it. So when you ask for a certificate for a new domain, devcert
will use the root CA credentials to generate a certificate specific to the
domain you requested, and returns the new certificate to you.
If you request a domain that has already had certificates generated for it,
devcert will simply return the cached certificates.
This setup ensures that browsers won't show scary warnings about untrusted
certificates, since your OS and browsers will now trust devcert's
certificates.
Security Concerns
There's a reason that your OS prompts you for your root password when devcert
attempts to install it's root certificate authority. By adding it to your
machine's trust stores, your browsers will automatically trust any certificate
generated with it.
This exposes a potential attack vector on your local machine: if someone else
could use the devcert certificate authority to generate certificates, and if
they could intercept / manipulate your network traffic, they could theoretically
impersonate some websites, and your browser would not show any warnings (because
it trusts the devcert authority).
To prevent this, devcert takes steps to ensure that no one can access the
devcert certificate authority credentials to generate malicious certificates
without you knowing. The exact approach varies by platform:
- macOS and Linux: the certificate authority's credentials are written to files that are only readable by the root user (i.e.
chown 0 ca-cert.crt
and
chmod 600 ca-cert.crt
). When devcert itself needs these, it shells out to
sudo
invocations to read / write the credentials. - Windows: because of my unfamiliarity with Windows file permissions, I
wasn't confident I would be able to correctly set permissions to mimic the setup
on macOS and Linux. So instead, devcert will prompt you for a password, and then
use that to encrypt the credentials with an AES256 cipher. The password is never
written to disk.
To further protect these credentials, any time they are written to disk, they
are written to temporary files, and are immediately deleted after they are no longer needed.
Additionally, the root CA certificate is unique to your machine only: it's
generated on-the-fly when it is first installed. ensuring there are no
central / shared keys to crack across machines.
Why install a root certificate authority at all?
The root certificate authority makes it simpler to manage which domains are
configured for SSL by devcert. The alternative is to generate and trust
self-signed certificates for each domain. The problem is that while devcert
is able to add a certificate to your machine's trust stores, the tooling to
remove a certificate doesn't cover every case. So if you ever wanted to
untrust devcert's certificates, you'd have to manually remove each one from
each trust store.
By trusting only a single root CA, devcert is able to guarantee that when you
want to disable SSL for a domain, it can do so with no manual intervention
- we just delete the domain-specific certificate files. Since these
domain-specific files aren't installed in your trust stores, once they are
gone, they are gone.
Integration
devcert has been designed from day one to work as low-level library that other
tools can delegate to. The goal is to make HTTPS development easy for everyone,
regardless of framework or library choice.
With that in mind, if you'd like to use devcert in your library/framework/CLI,
devcert makes that easy.
In addition to the options above, devcert exposes a ui
option. This option
allows you to control all the points where devcert requries user interaction,
substituting your own prompts and user interface. You can use this to brand
the experience with your own tool's name, localize the messages, or integrate
devcert into a larger existing workflow.
The ui
option should be an object with the following methods:
{
async getWindowsEncryptionPassword(): Promise<string> {
},
async warnChromeOnLinuxWithoutCertutil(): Promise<string> {
},
async closeFirefoxBeforeContinuing() {
},
async startFirefoxWizard(certificateHost: string) {
},
async firefoxWizardPromptPage(certificateURL: string): Promise<string> {
}
async waitForFirefoxWizard() {
}
}
You can supply any or all of these methods - ones you do not supply will fall
back to the default implemenation.
Testing
Testing a tool like devcert can be a pain. I haven't found a good automated
solution for cross platform GUI testing (the GUI part is necessary to test
each browser's handling of devcert certificates, as well as test the Firefox
wizard flow).
To make things easier, devcert comes with a series of virtual machine images. Each one is a snapshot taken right before running a test - just launch the machine and hit .
You can also use the snapshotted state of the VMs to roll them back to a
pristine state for another round of testing.
Note: Be aware that the macOS license terms prohibit running it on
non-Apple hardware, so you must own a Mac to test that platform. If you don't
own a Mac - that's okay, just mention in the PR that you were unable to test
on a Mac and we're happy to test it for you.
Virtual Machine Snapshots
License
MIT © Dave Wasmer