Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gatsby): Add self-signed cert to node trust store (https) #18703

Merged
merged 33 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
12accb6
Update devcert to point to my dev repo
Oct 15, 2019
5540d13
Update program.host to reflect server certificate
Oct 15, 2019
cc82fcc
Trust self signed certificates
Oct 15, 2019
6e99fae
Destructure ssl data and trust ca
Oct 15, 2019
393429e
keyPath/certPath are not used outside of this module
Oct 15, 2019
a126ffb
Use more robust encryption password routine
Oct 15, 2019
c0a4ac8
Allow defining ca certificate from cli
Oct 16, 2019
77530fe
Use cli defined ca certificate, if it exists
Oct 16, 2019
1edfef2
prompt verbiage
Oct 16, 2019
73cf13f
Update docs
Oct 16, 2019
dc486a5
Change --ca to --ca-file
Oct 16, 2019
730d980
Updated devcert/certutil setup process
Js-Brecht Oct 16, 2019
e56a7c5
Missed relative -> absolute path
Js-Brecht Oct 16, 2019
ebce67d
certutil is completely ignored on Windows
Js-Brecht Oct 16, 2019
5b0533c
Align usage with `npm run develop`
Js-Brecht Nov 22, 2019
0483702
Renamed imported function/destructured returned value
Js-Brecht Nov 26, 2019
73d61cc
Use updated devcert api
Js-Brecht Nov 26, 2019
ff0f4c8
Separated getWindowsEncryptionPassword function
Js-Brecht Nov 26, 2019
76f1197
Updated tests/snapshot to work with new devcert API
Js-Brecht Nov 26, 2019
9a60c68
Doc formatting
Js-Brecht Nov 26, 2019
91742a4
Allow defining ca certificate from cli
Oct 16, 2019
04a6074
prettier code style formatting
Js-Brecht Jan 29, 2020
07dc617
keyPath / certPath are not being used.
Js-Brecht Jan 29, 2020
cf381df
install certutil automatically (if possible)
Js-Brecht Mar 9, 2020
e360131
devcert api call test fix
Js-Brecht Mar 9, 2020
5c7caa2
use braces
Js-Brecht Mar 10, 2020
2398966
Updated docs per suggestions
Js-Brecht Mar 10, 2020
ea47440
all-caps HTTPS
Js-Brecht Mar 10, 2020
edc486a
doc: devcert-cli change
Js-Brecht Mar 10, 2020
4296400
restructure doc
Mar 10, 2020
fffaa0d
Merge branch 'devcert-upgrade' of https://github.com/Js-Brecht/gatsby…
Mar 10, 2020
f2ac0a1
missed an HTTPS
Js-Brecht Mar 10, 2020
daed896
Merge branch 'master' into devcert-upgrade
wardpeet Apr 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 120 additions & 22 deletions docs/docs/local-https.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,148 @@ Gatsby provides an easy way to use a local HTTPS server during development, than

Start the development server using `npm run develop` as usual, and add either the `-S` or `--https` flag.

$ npm run develop -- --https
```shell
$ npm run develop -- --https
```

## Setup

When setting up a development SSL certificate for the first time, you may be asked to type in your password after starting the development environment:

info setting up SSL certificate (may require sudo)
```text
info setting up SSL certificate (may require elevated permissions/sudo)

Password:
```

On windows, the prompt will differ:

```text
A password is required to access the secure certificate authority key
used for signing certificates.

If this is the first time this has run, then this is to set the password
for future use. If any new certificates are signed later, you will need
to use this same password.

Please enter the CA password:
```

The password is _only_ required the first time you are using Gatsby's HTTPS feature on your machine, or when you are creating a brand new certificate.

After typing in your password, `devcert` will install the CA certificate in your operating system trusted certs store. A utility called `certutil` will be needed to update the trust store for various browsers; specifically: Firefox, and Chrome (when it's running on Linux).

### In case `certutil` is not installed on your machine

`devcert` is configured to install `certutil` automatically, unless you're running Windows. If an automatic install is not successful, you may need to install it manually.

- To install `certutil`, you need to install the `nss tools` package(s). The exact procedure will differ depending on your operating system.

- On a linux OS, you should be able to run one of the following, depending on your Linux distro:

```shell
# Debian based (Ubuntu)
sudo apt install libnss3-tools

# RHEL based (Fedora)
sudo yum install nss-tools

# OpenSuse
sudo zypper install mozilla-nss-tools
```

- On MacOS, you should be able to run:

Password:
```shell
brew install nss
```

This is _only_ required the first time you are using Gatsby's HTTPS feature on your machine. After that, certificates will be created on the fly.
- With regards to Windows: Precompiled libraries are rare, so you may need to compile it yourself. Because of how difficult Windows makes it, `devcert` will not attempt to update the Firefox trust store automatically; instead, it will fall back to using the "Firefox wizard", detailed below.

After typing in your password, `devcert` will attempt to install some software necessary to tell Firefox (and Chrome, only on Linux) to trust your development certificates.
If you choose not to install `certutil`, or the automatic install is not successful, you may get the following errors/prompts:

Unable to automatically install SSL certificate - please follow the
prompts at http://localhost:52175 in Firefox to trust the root certificate
See https://github.com/davewasmer/devcert#how-it-works for more details
-- Press <Enter> once you finish the Firefox prompts --
- If you use Chrome on Linux:

If you wish to support Firefox (or Chrome on Linux), visit `http://localhost:52175` in Firefox and follow the point-and-click wizard. Otherwise, you may press enter without following the prompts. **Reminder: you'll only need to do this once per machine.**
```text
WARNING: It looks like you have Chrome installed, but you specified
'skipCertutilInstall: true'. Unfortunately, without installing
certutil, it's impossible get Chrome to trust devcert's certificates
The certificates will work, but Chrome will continue to warn you that
they are untrusted.
```

Now open the development server at `https://localhost:8000` and enjoy the HTTPS goodness ✨. Of course, you may change the port according to your setup.
- If you have Firefox installed, `devcert` will try to utilize Firefox itself to trust the certificate

```text
devcert was unable to automatically configure Firefox. You'll need to
complete this process manually. Don't worry though - Firefox will walk
you through it.

When you're ready, hit any key to continue. Firefox will launch and
display a wizard to walk you through how to trust the devcert
certificate. When you are finished, come back here and we'll finish up.
(If Firefox doesn't start, go ahead and start it and navigate to
http://localhost:52175 in a new tab.)

If you are curious about why all this is necessary, check out
https://github.com/davewasmer/devcert#how-it-works
<Press any key to launch Firefox wizard>
```

- You can press enter here, and it will launch Firefox for you.

- If you wish to have trust support on Firefox, tell the point-and-click wizard `this certificate can identify websites`, and click OK. Otherwise, you may hit cancel and close the browser, then key return to finish building. **Reminder: you'll only need to do this once per machine.**

### After `devcert` setup process

You can open the development server at `https://localhost:8000` and enjoy the HTTPS goodness ✨. Of course, you may change the port according to your setup.

Find out more about [how devcert works](https://github.com/davewasmer/devcert#how-it-works).

### Management of certificates generated by devcert

If you want to do some maintenance/cleanup of the certificates generated by `devcert`, please refer to [devcert-cli](https://github.com/davewasmer/devcert-cli/blob/master/README.md)
Js-Brecht marked this conversation as resolved.
Show resolved Hide resolved

## Custom Key and Certificate Files

You may find that you need a custom key and certificate file for https if you use multiple
You may find that you need a custom key and certificate file for HTTPS if you use multiple
machines for development (or if your dev environment is containerized in Docker).

If you need to use a custom https setup, you can pass the `--https`, `--key-file` and
`--cert-file` flags to `npm run develop`.
If you need to use a custom HTTPS setup, you can pass the `--https`, `--key-file`,
`--cert-file`, and `--ca-file` flags to `npm run develop`.

- `--cert-file` [relative path to ssl certificate file]
- `--key-file` [relative path to ssl key file]
- `--cert-file` [relative/absolute path to ssl certificate file]
- `--key-file` [relative/absolute path to ssl key file]
- `--ca-file` [relative/absolute path to ssl certificate authority file]

See the example command:
See the example commands below:

```shell
gatsby develop --https --key-file ../relative/path/to/key.key --cert-file ../relative/path/to/cert.crt
```
- Using `npm run develop`

```shell
# Using relative paths
$ npm run develop -- --https --key-file ../relative/path/to/key.key --cert-file ../relative/path/to/cert.crt --ca-file ../relative/path/to/ca.crt

# Or using absolute paths
$ npm run develop -- --https --key-file /absolute/path/to/key.key --cert-file /absolute/path/to/cert.crt --ca-file /absolute/path/to/ca.crt
```

- Alternatively, you can run the development server using the gatsby cli

```shell
# Using relative paths
$ gatsby develop --https --key-file ../relative/path/to/key.key --cert-file ../relative/path/to/cert.crt --ca-file ../relative/path/to/ca.crt

# Or using absolute paths
$ gatsby develop --https --key-file /absolute/path/to/key.key --cert-file /absolute/path/to/cert.crt --ca-file /absolute/path/to/ca.crt
```

Usage of the `--ca-file` flag is only required if your certificate is signed by a certificate authority.

If your certificate is self-signed, then do not include the `--ca-file` flag. Also, if you want your browser to trust a self-signed certificate, you will need to add it to your operating system (or browser's, in Firefox's case) root certificate store for your browser to trust it.

in most cases, the `--https` passed by itself is easier and more convenient to get local https.
In most cases, the `--https` passed by itself is easier and more convenient to get local https.

---

Keep in mind that the automatic certificates issued with the `--https` flag are explicitly issued to `localhost` and will only be accepted there. Using it together with the `--host` option will likely result in browser warnings.
Automatic certificates issued with the `--https` flag are issued to `localhost` by default, unless you have used the `--host` flag. If you have, a record in your hosts file will automatically be configured to point the defined host to `127.0.0.1`. At this time, ip addresses defined by `--host` are not supported.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Automatic certificates issued with the `--https` flag are issued to `localhost` by default, unless you have used the `--host` flag. If you have, a record in your hosts file will automatically be configured to point the defined host to `127.0.0.1`. At this time, ip addresses defined by `--host` are not supported.
Automatic certificates issued with the `--https` flag are issued to `localhost` by default, unless you have used the `--host` flag. If you have, a record in your hosts file, it will automatically be configured to point the defined host to `127.0.0.1`. At this time, ip addresses defined by `--host` are not supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should "a record in your hosts file, " be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About removing that phrase: this process is not contingent on there being a record in the hosts file, so saying "if you have a record in your hosts file, it will automatically be configured" wouldn't be accurate.

If we remove "a record in your hosts file, ", then "it" becomes kind of vague in this context... what is "it"? If the goal is to make it a bit less technical, maybe it could read?

- If you have, it will automatically be configured
+ If you have, your device will automatically be configured

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wardpeet any thoughts?

9 changes: 7 additions & 2 deletions packages/gatsby-cli/src/create-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,18 @@ function buildLocalCommands(cli, isLocalSite) {
alias: `cert-file`,
type: `string`,
default: ``,
describe: `Custom HTTPS cert file (relative path; also required: --https, --key-file). See https://www.gatsbyjs.org/docs/local-https/`,
describe: `Custom HTTPS cert file (also required: --https, --key-file). See https://www.gatsbyjs.org/docs/local-https/`,
})
.option(`k`, {
alias: `key-file`,
type: `string`,
default: ``,
describe: `Custom HTTPS key file (relative path; also required: --https, --cert-file). See https://www.gatsbyjs.org/docs/local-https/`,
describe: `Custom HTTPS key file (also required: --https, --cert-file). See https://www.gatsbyjs.org/docs/local-https/`,
})
.option(`ca-file`, {
type: `string`,
default: ``,
describe: `Custom HTTPS CA certificate file (also required: --https, --cert-file, --key-file). See https://www.gatsbyjs.org/docs/local-https/`,
})
.option(`open-tracing-config-file`, {
type: `string`,
Expand Down
2 changes: 1 addition & 1 deletion packages/gatsby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"debug": "^3.2.6",
"del": "^5.1.0",
"detect-port": "^1.3.0",
"devcert": "^1.0.2",
"devcert": "^1.1.0",
"dotenv": "^8.2.0",
"eslint": "^6.7.2",
"eslint-config-react-app": "^5.1.0",
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/src/commands/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ module.exports = async (program: IProgram): Promise<void> => {

program.ssl = await getSslCert({
name: sslHost,
caFile: program[`ca-file`],
certFile: program[`cert-file`],
keyFile: program[`key-file`],
directory: program.directory,
Expand Down
2 changes: 0 additions & 2 deletions packages/gatsby/src/commands/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { PackageJson, Reporter } from "gatsby"

export interface ICert {
keyPath: string
certPath: string
key: string
cert: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`gets ssl certs Custom SSL certificate loads a cert from a absolute paths 1`] = `
exports[`gets ssl certs Custom SSL certificate loads a cert from absolute paths 1`] = `
Object {
"cert": "/foo.crt::file",
"certPath": "/foo.crt",
"key": "/foo.key::file",
"keyPath": "/foo.key",
}
`;

exports[`gets ssl certs Custom SSL certificate loads a cert relative to a directory 1`] = `
Object {
"cert": "/app/directory/foo.crt::file",
"certPath": "/app/directory/foo.crt",
"key": "/app/directory/foo.key::file",
"keyPath": "/app/directory/foo.key",
}
`;

Expand Down Expand Up @@ -57,7 +53,7 @@ Array [
exports[`gets ssl certs automatic SSL certificate sets up dev cert 1`] = `
Array [
Array [
"setting up automatic SSL certificate (may require sudo)
"setting up automatic SSL certificate (may require elevated permissions/sudo)
",
],
]
Expand Down
16 changes: 10 additions & 6 deletions packages/gatsby/src/utils/__tests__/get-ssl-cert.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ jest.mock(`devcert`, () => {
}
})

const { certificateFor } = require(`devcert`)
const getDevCert = require(`devcert`).certificateFor
const reporter = require(`gatsby-cli/lib/reporter`)
const getSslCert = require(`../get-ssl-cert`)

describe(`gets ssl certs`, () => {
beforeEach(() => {
reporter.panic.mockClear()
reporter.info.mockClear()
certificateFor.mockClear()
getDevCert.mockClear()
})
describe(`Custom SSL certificate`, () => {
it.each([[{ certFile: `foo` }], [{ keyFile: `bar` }]])(
Expand All @@ -46,7 +46,7 @@ describe(`gets ssl certs`, () => {
})
).resolves.toMatchSnapshot()
})
it(`loads a cert from a absolute paths`, () => {
it(`loads a cert from absolute paths`, () => {
expect(
getSslCert({
name: `mock-cert`,
Expand All @@ -60,13 +60,17 @@ describe(`gets ssl certs`, () => {
describe(`automatic SSL certificate`, () => {
it(`sets up dev cert`, () => {
getSslCert({ name: `mock-cert` })
expect(certificateFor).toBeCalledWith(`mock-cert`, {
installCertutil: true,
expect(getDevCert).toBeCalledWith(`mock-cert`, {
getCaPath: true,
skipCertutilInstall: false,
ui: {
getWindowsEncryptionPassword: expect.any(Function),
},
})
expect(reporter.info.mock.calls).toMatchSnapshot()
})
it(`panics if certificate can't be created`, () => {
certificateFor.mockImplementation(() => {
getDevCert.mockImplementation(() => {
throw new Error(`mock error message`)
})
getSslCert({ name: `mock-cert` })
Expand Down
50 changes: 43 additions & 7 deletions packages/gatsby/src/utils/get-ssl-cert.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const report = require(`gatsby-cli/lib/reporter`)
const fs = require(`fs`)
const path = require(`path`)
const os = require(`os`)
const prompts = require(`prompts`)

const absoluteOrDirectory = (directory, filePath) => {
// Support absolute paths
Expand All @@ -11,7 +12,28 @@ const absoluteOrDirectory = (directory, filePath) => {
return path.join(directory, filePath)
}

module.exports = async ({ name, certFile, keyFile, directory }) => {
const getWindowsEncryptionPassword = async () => {
report.info(
[
`A password is required to access the secure certificate authority key`,
`used for signing certificates.`,
``,
`If this is the first time this has run, then this is to set the password`,
`for future use. If any new certificates are signed later, you will need`,
`to use this same password.`,
``,
].join(`\n`)
)
const results = await prompts({
type: `password`,
name: `value`,
message: `Please enter the CA password`,
validate: input => input.length > 0 || `You must enter a password.`,
})
return results.value
}

module.exports = async ({ name, certFile, keyFile, caFile, directory }) => {
// check that cert file and key file are both true or both false, if they are both
// false, it defaults to the automatic ssl
if (certFile ? !keyFile : keyFile) {
Expand All @@ -25,15 +47,18 @@ module.exports = async ({ name, certFile, keyFile, directory }) => {
const keyPath = absoluteOrDirectory(directory, keyFile)
const certPath = absoluteOrDirectory(directory, certFile)

process.env.NODE_EXTRA_CA_CERTS = caFile
? absoluteOrDirectory(directory, caFile)
: certPath
return await {
keyPath,
certPath,
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
}
}

report.info(`setting up automatic SSL certificate (may require sudo)\n`)
report.info(
`setting up automatic SSL certificate (may require elevated permissions/sudo)\n`
)
try {
if ([`linux`, `darwin`].includes(os.platform()) && !process.env.HOME) {
// this is a total hack to ensure process.env.HOME is set on linux and mac
Expand All @@ -46,10 +71,21 @@ module.exports = async ({ name, certFile, keyFile, directory }) => {
const mkdtemp = fs.mkdtempSync(path.join(os.tmpdir(), `home-`))
process.env.HOME = mkdtemp
}
const certificateFor = require(`devcert`).certificateFor
return await certificateFor(name, {
installCertutil: true,
const getDevCert = require(`devcert`).certificateFor
const { caPath, key, cert } = await getDevCert(name, {
getCaPath: true,
skipCertutilInstall: false,
ui: {
getWindowsEncryptionPassword,
},
})
if (caPath) {
process.env.NODE_EXTRA_CA_CERTS = caPath
}
return {
key,
cert,
}
} catch (err) {
report.panic({
id: `11522`,
Expand Down
Loading