Joshua's Docs - Self-Signed SSL Certificates / OpenSSL Cheatsheet and Guide
Light

🚨 This page covers approaches for self-signed certs; these are useful for local dev work, testing, and special use-cases. For actual production websites, you should not be using self-signed certificates - if you want a free alternative to paid SSL certs, I cannot recommend Let's Encrypt enough!

Table of Contents

Self-Signed SSL Certificates: Basic Requirements

The basic requirements for local SSL hosting, and setting up self-signed SSL in general, are:

  • A private key (.pem)
  • A CSR (Certificate Signing Request)
  • A public certificate (usually .crt, sometimes .pem too)

The CSR is kind of intermediate step between the key and the cert (the CSR is signed by the key to become the cert, so in many cases it is temporarily created via a prompt). You can think of the cert as being derived from the key, via the CSR.

💡 SSL cert pairs are often referred to with the X.509 identifier; this is essentially the standard that is used for TLS/SSL, but it is used for other purposes as well.

However, how you generate these is a little more complicated than how you might normally generate key-pairs with OpenSSL, since it is best practice to provide more configuration inputs to create the certificate, such as DNS entries that can use the certificate. There are also different approaches, based on how you want to use the cert you are generating.

💡 If you are on Windows, there is a handy built-in Powershell Cmdlet that simplifies generating a local dev certificate a lot! - the New-SelfSignedCertificate command.

💡 Also, don't forget that if you want to use a custom local domain (like using mydomain.test instead of localhost) you need to edit your local HOSTS file to add an entry resolving localhost (or 127.0.0.1) to that custom local domain.

Using OpenSSL to Generate Self-Signed Certs

Before you get started with any of the above approaches, you are going to need the actual OpenSSL binary. There are pretty good chances you might already have it installed (especially if you do a fair amount of dev work) - try running where openssl (Windows), or which openssl (Unix).

If you need a new copy, you can either build from source, or grab a pre-built binary.

You can check the version of local binary with openssl version.

Now, on to how to actually use OpenSSL to generate your own cert! 👇

OpenSSL - Temporary CSR - Prompt Based

With this method, you don't need to create a CSR as a separate step; instead OpenSSL will prompt you (via the CLI) for the minimum information it needs to temporarily create a CSR, then use that temporary CSR with your private key to generate cert.

This method is commonly recommended, because it involves the least steps.

openssl req -newkey rsa:4096 -nodes -keyout domain.key -x509 -sha256 -days 365 -out domain.crt

Explanation of command parts:

  • req: This is the main command we are passing to OpenSSL - it asks OpenSSL to generate a CSR for us. (Docs).

  • -newkey: Says that we want both a new CSR and a new private key

    • rsa:4096: algorithm and number of bits to use when creating the private key. Many versions of OpenSSL now use 2048 as the default bit size
    • -nodes: Tells OpenSSL not to encrypt the private key with a password / passphrase. Normally, this is a bad idea, but since this is a throwaway local self-signed cert for development purposes, adding a password adds unnecessary steps to our setup.
    • -keyout domain.key: Save the generated private key to domain.key file. We can use any filname we want
  • -x509: Important: This tells OpenSSL that the output of our command should be a Self-Signed Certificate, not the CSR used to generate it.

    • -sha256: Explicitly specifies the algorithm, for the message digest, used to sign the CSR and generate the final certificate. The default is now sha256, but it is worth explicitly declaring, since older versions default to insecure MD5.
    • -days 365: How long the cert should be valid for, before it expires.

      • If you are going to add a permanent exception to your browser to trust the local cert, you should probably put this to a shorter value to avoid adding too much extra risk
    • out domain.crt: Tells OpenSSL where to save our generated cert file. You can use any filename.

OpenSSL - Generating Self-Signed Cert Without Prompts

This is very similar to the above approach, but instead of filling out the CLI Q&A prompt that OpenSSL gives us, we can pass in pre-configured values to use with the CSR. This is useful if we want to be able to share a configuration that other devs can use to generate their own local certs, create a base template config, etc.

You can pass configuration values either entirely through the CLI, as arguments, or by giving OpenSSL the filename of a plain-text file where you have saved the values (in a specific config file format).

OpenSSL - Self-Signed Cert Without Prompt - Using Arguments

📄 Here is a doc page specifically for x509 (cert) arguments

Argument Based:

# Broken out
openssl req \
	-newkey rsa:4096 -nodes \
		-keyout domain.key \
	-x509 \
		-sha256 -days 365 \
		# Here is where we start adding the config stuff,
		# ,which would normally be passed via prompt
		# --- Distinguished Name Section ---
		-subj "/C=US/ST=WA/L=SEATTLE/O=MyCompany/OU=MyDivision/CN=*.domain.test" \
		# --- SAN (subject alternative names) Section ---
		# Notice that multiple values are comma separated; do not try to add
		#     multiple extension entries for the same extension
		# This syntax requires >= v1.1.1
		-addext "subjectAltName = DNS:*.domain.test, DNS:localhost, DNS:127.0.0.1, DNS:mail.domain.test" \
		# ^ If you are on v <= v1.1.0, this is notoriously difficult to pass via
		#    CLI, as it appears extension values must be passed via conf file.
		#    - See: https://security.stackexchange.com/a/91556/248319
		# Same as before
		-out domain.crt \

# Same as above, but all together:
openssl req -newkey rsa:4096 -nodes -keyout domain.key -x509 -sha256 -days 365 -subj "/C=US/ST=WA/L=SEATTLE/O=MyCompany/OU=MyDivision/CN=*.domain.test" -addext "subjectAltName = DNS:*.domain.test, DNS:localhost, DNS:127.0.0.1, DNS:mail.domain.test" -out domain.crt

If the above command failed for you (perhaps with req: Unknown digest addext), check your version of OpenSSL (with openssl version). If it is below v1.1.1, you either need to use a config file to pass in extension values (see below), or use some tricky bash sub-shell stuff.

OpenSSL - Self-Signed Cert Without Prompt - Using Config File

These files are usually called __.conf (e.g. mydomain.conf), and they follow a specific INI-like format. The syntax can be a little confusing (here is an unofficial doc page that is helpful).

One really important thing to note about the config file syntax is that often values are passed by section lookup, rather than directly. So, for example, if we wanted to pass custom alternative names (DNS entries), we would pass a pointer to the section of the config where we have entered those values.

In this scenario, we can use any header name we want, but assuming we went with alternate_names, we would need a key-pair under our extensions with subjectAltName = @alternate_names, and then a section with the header [alternate_names ], under which our DNS entries are stored (e.g. DNS.1 = localhost).

📄 For config keys that go with req, see the bottom of the req man page

📄 Here is a doc page specifically for x509 (cert) config sections

To actually load the config values, use the -config argument, and pass your filename. For example, here is the typical command to generate a private key and self-signed cert, with input from a config.

openssl req -config myconfig.conf -newkey rsa -x509 -days 365 -out domain.crt

Example Config File, with comments:

# Key:
#  - <SK> = Key to a config subsection

# Main entry point
# since our command on the CLI is `req`, OpenSSL is going to look for a matching entry-point
# This lets you store multiple command configs together, in a single file
[req]
# algorithm and number of bits to use when creating the private key
# rsa:2048
default_bits = 2048
# Same as `-nodes` argument, to prevent encryption of private key (passphrase)
encrypt_key = no
# Explicitly tells OpenSSL which message digest algorithm to use
# Good practice to specify, since older versions might default to MD5 (insecure)
default_md = sha256
# If you don't use `-keyout` in the CLI, this determines the private key filename
default_keyfile = domain.key
# <SK> These are values that are used to *distinguish* the certificate, such as the country and organization
# These values are normally collected via Q&A prompt in the CLI if config file is not used
distinguished_name = req_distinguished_name
# Ensures that distinguished_name values will be pulled from this file, as
#     opposed to prompting the user in the CLI
prompt = no
# <SK> Used for extensions to the self-signed cert OpenSSL is going to generate for us
x509_extensions = x509_extensions

[req_distinguished_name]
# - These are all values that are used to *distinguish* the certificate, such as 
#     the country and organization
# - Many of these have shorter keys that should be used for non-prompt values,
#     and long keys that should have a prompt string to display to the user, and
#     optionally a default value if the prompt is skipped (see below note)
# - For long keys, if you use fieldName with `_default` at the end, the value
#     will be used if prompt!==true, or if the user skips the prompt in the CLI

# Long = countryName
C = US
# Long = stateOrProvinceName
ST = WA
# Long = localityName
L = Seattle
# Long = organizationName
O = MyCompany
# Long = organizationalUnitName
OU = MyDivision
# Long = commonName
# Pay extra attention to common name - You can only define one, and it is the
#     value that is displayed to the user. Should NOT include protocol, but can
#     be in format of domain.tld, www.domain.tld, or even wildcard, to share a
#     common cert across multiple subdomains - `*.domain.tld`.
# Also, any value that you use here !*** MUST ***! be ALSO included in the SAN
#     (subject alternative name) section (subjectAltName), if you choose to
#     include that section. See: https://stackoverflow.com/a/25971071/11447682
CN = *.domain.test

[x509_extensions]
# <SK> Used for (generically) custom field-value pairs that should be associated
#   with the cert, such as extra DNS names, IP addresses, and emails
subjectAltName = @alternate_names

[alternate_names]
# Extra domain names to associate with our cert
#  - These can be a mix of wildcard, IP address, subdomain, etc.
DNS.1 = *.domain.test
DNS.2 = localhost
DNS.3 = 127.0.0.1
DNS.4 = mail.domain.test
# Etc.
# See:
# - https://www.openssl.org/docs/man1.1.1/man5/x509v3_config.html#Subject-Alternative-Name
# - https://en.wikipedia.org/wiki/Subject_Alternative_Name
Above Example Config, Without Comments
[req]
default_bits = 2048
encrypt_key = no
default_md = sha256
default_keyfile = domain.key
distinguished_name = req_distinguished_name
prompt = no
x509_extensions = x509_extensions

[req_distinguished_name]
C = US
ST = WA
L = Seattle
O = MyCompany
OU = MyDivision
CN = *.domain.test

[x509_extensions]
subjectAltName = @alternate_names

[alternate_names]
DNS.1 = *.domain.test
DNS.2 = localhost
DNS.3 = 127.0.0.1
DNS.4 = mail.domain.test

OpenSSL - Saving a Generated CSR

If you want to save the generated CSR, instead of having OpenSSL just temporarily use and throw away the one it uses to create a cert, you just need to change a few arguments.

  • Remove -x509 (this targets Cert as output, not CSR, so don't want it)
  • Change -out domain.crt to -out domain.csr (or whatever filename you prefer)

You can export the CSR while:

  • also generating a new private key

    • keep -newkey and -keyout
  • or with an existing private key

    • Remove -newkey and -keyout, add -key mykeyfile.key

Since the Cert is created by signing CSR values with your key, you can also reverse the process, and actually generate a CSR from an existing public certificate, assuming you have the private key:

openssl x509 -in domain.crt -signkey domain.key -x509toreq -out domain.csr

OpenSSL - Using an Existing Saved CSR File to Generate a New Certificate

If you already have a saved CSR and private key, you skip providing all the information needed to generate the CSR:

  • Remove -newkey and -keyout, remove `-out

    OpenSSL - Verifying and Viewing SSL Info

  • CSR

    • openssl req -text -noout -verify -in domain.csr
  • Certificate

    • openssl x509 -text -noout -in domain.crt

Generating Certificates: Scripts and Generator Tools

🚨 Be very careful about tools (and try to avoid them) that work by installing a global / root level CA / certificate; this opens a large security hole in your dev environment. See this post for an explanation.

📄 If you find it easier to read code, rather than bash commands, you might find it helpful to look at how mockttp handles TLS.

These are tools that can make creating local certs a little less painful than a bunch of hard to remember commands. Most of these are just wrappers around OpenSSL.

Local Hosting - Trusting a Self-Signed Cert

Once you have your certificate generated, that's not actually the end of the work necessary to use SSL locally. Because browsers (generally) don't trust self-signed certificates, you need to an exception to allow your specific certificate to be trusted.

As a best practice, trusting a local self-signed certificate should be done temporary, and on a per-site basis (not with a root / global / shared CA).

Temporary certificate trusting:

  1. Get the fingerprint of the cert

    • openssl x509 -pubkey -noout -in {CERT_PATH} | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 (credit)

      • Or
    • openssl x509 -pubkey < {CERT_PATH} | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64 (Credit)
  2. Launch Chrome, temporarily, with the fingerprint of the cert to trust:

    • chrome --ignore-certificate-errors-spki-list={FINGERPRINT}

      • OR
    • chrome --ignore-certificate-errors-spki-list=$(cat fingerprints.txt)
    • Also, you can pass your domain as the last argument to have it automatically open to that page

💡 For Chrome, you probably also want / need to have it force opened in a new instance, rather than your normal browser. You can do this with either chrome --user-data-dir=\tmp, or, on Windows, chrome --user-data-dir=%TEMP%

Other Resources

Other guides
Markdown Source Last Updated:
Sun Jan 03 2021 03:07:06 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Sat Jan 02 2021 14:53:24 GMT+0000 (Coordinated Universal Time)
© 2021 Joshua Tzucker, Built with Gatsby
Feedback