TLS certificates for Local Area Networks
tlshttpslan2021-06-11 - carpie

In this article, we're going to look at how to host HTTPS servers, limited to our local LAN without that annoying security warning.

These days, serving anything in clear text is considered bad practice and rightfully so! For HTTP servers the standard answer is to use Transport Layer Security (or TLS). This means you get a certificate for your server signed by an authority your browser already trusts. With services like Let's Encrypt, this is now free, automated, and easy to do.

The problem

Sadly, if you are deploying on your Local Area Network (LAN) you cannot get a signature from a pre-trusted authority, so your browser is always going to hate on your internal server. This has been a problem for decades and the problem still remains. The standard answer is to create your own internal certificate authority (CA) and install its public certificate in the browsers of everyone on your LAN. Sadly, this is still the answer.

This sounds daunting and indeed, creating a full-blown CA comes with additional security and management responsibilities. However, in this article, I am going to show you a simple technique you can use for one-off server deploys to satisfy the brower's security check without having to become a full-blown certificate authority. I'll also add some bonus information you can use to expand the technique beyond a single server deploy.

DISCLAIMER: Before we get started, I must make a few disclaimers. When discussing the following technique I will share my thoughts on the security concerns of the technique. However, I am not a security expert and claim no guarantees on the safety of using this technique. If the information and/or servers you need to protect need strong security you should seek the advice of a professional security engineer. The information I share here is for educational purposes only and you are responsible for your own security.

Ok! Let's get started...

Materials needed

This article is mostly informational, so there are no requirements for following along. I will be demonstrating the techniques though. If you want to re-create the demonstrations you will need openssl, docker, and docker-compose installed and a working internet connection to download docker images.

Understanding how the browser connects to an HTTPS server

When I demonstrate something, I want my listeners to understand what it is I'm doing as opposed to just blindy copying and pasting, so I want to cover at a high-level what happens when a browser connects to an HTTPS server. If you already know or are just impatient, jump to the action section.

Browser HTTPS Connections

In this example, we have carpie.net, a public HTTPS server, and host1.home.lan, a LAN HTTPS server. The only difference is how our TLS certificates are signed. The public one was signed by an authority the browser already trusts and the LAN one, we signed ourselves. Let's see what happens when try to visit each server with our browser.

When your browser attempts to make a connection to a TLS protected server, the browser will start what's called a TLS handshake. Responding to this handshake, the server will include its TLS certificate. The browser checks the signature of the certificate against a trusted set of certificate authorities. This set is sometimes built into the browser or in some cases is pulled from a similar operating system itself. In the public case, the certificate signature check passes because the CA is trusted. In our LAN case, it fails because our certificate is not signed by a trusted CA.

The browser also does some additional checks as well, such as checking that the common name field matches the host name. Modern browsers such as Chrome have started requiring the Subject Alternative Name extension field as well, so we'll include this in our certificates.

Ok, let's see how to set up a LAN server and get our browser to trust it.

The one-shot CA technique

Ok, for the server, we know we need a certificate. To do this we need to create a private and public key. From the public key, we create a signing request and have it signed by a certificate authority. The result of the signing is the server's certificate. We've already determined that no public pre-trusted CA is going to sign a certificate for an internal private server, so we'll have to create our own. Then, we'll have to tell our browser to trust our CA as well.

Running your own CA comes with responsibilities. Once you tell a browser to trust it, it will. So, anything signed by the CA will be trusted. This means you need to tightly control the CA secret key and manage lists of certificate that have been revoked. Otherwise, if the key is compromised, then an attacker can sign nefarious certificates and trick our browser into trusting them. That's a lot of additional effort we don't really want to do for own one-off internal server.

One technique to get the same level of security and trust but avoid the responsibility is to create a one-shot CA. This means we'll create a CA private key and public certificate, sign a single server certificate and then delete the CA's private key. We can then add the public CA certificate as a trusted authority to our browser and it will then trust our signed server certificate. However, since we deleted the CA's private key, no other certificates can ever be signed by that CA. It's a single use trusted CA.

It sounds much more complicated than it really is, so let's see it in action...

How to create a one-shot CA

First, we'll setup the CA.

openssl genrsa -aes256 -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.pem -subj "/CN=MyCA"

You will be prompted for a password to protect the key. This password will be used for creating the CA certificate and for signing the server certificate.

These operations will create two files, ca.key which is the private key for the CA (used for signing) and ca.pem, the public certificate key for the CA.

The first command created a common 4096 bit RSA secret key using the AES 256 cipher. The second command used the private to key to create a public x509 certificate that can be freely shared. The -days parameter specificies how long the certificate is valid for, which basically specifies how long the browser will trust it. We used 10 years here to make a long lived certificate for our internal network. You can use a different value if you wish. The -subj parameter provides information fields in the certificate. There are a whole host of fields you can fill out but they are not required, so we used the minimum and just specified Common Name (CN).

Ok, our CA is created, let's create the server key and certificate signing request. These two steps are independent of the CA.

openssl req -nodes -newkey rsa:2048 -keyout server.key -out server.csr -subj="/CN=host1.home.lan"

This command creates a new 2048-bit RSA key and a certificate signing request (CSR) all in a single command. Again we pass in Common Name. It is important that the common name be the host name of the internal server we are creating a certificate for. In this case, our internal host, arbitrarily named host1.home.lan.

Now that we have the CSR, we need to sign it with our CA key. We need to add that "Subject Alternative Name" field though for newer browsers. Unfortunately, with current OpenSSL versions, we cannot specify those as a parameter, so we need to create a file, v3.ext that will contain the extensions.

# v3.ext
[EXT]
subjectAltName=DNS.0:host1.home.lan

Now we can sign the certificate, again using the minimal amount of informational fields.

openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.crt \
-days 3650 -extensions EXT -extfile v3.ext

This command invokes the x509 certificate signer to sign server.csr with the CA certificate and key we created early. We pass in CAcreateserial to keep openssl from griping about a missing serial number file. We specify the output file to be server.crt. Again, we sign for 10 years to keep from having to do this again anytime soon. Again, the length is totally up to you. Finally we specify that the x509 extensions from the EXT section of the v3.ext extension file should be added to the certificate.

The resulting output, server.crt, is our HTTPS server certificate. We will need that and the server.key file for spinning up our HTTPS server. Remember, the .crt file is public, and the .key file is secret and should be protected.

Now, let's clean up the files we don't need. Also, for security reasons, we'll remove the secret key for the CA itself.

rm -f ca.srl server.csr ca.key

By removing the ca.key, we can no longer sign certificates without creating a new CA key. We can still use the CA certificate ca.pem for trust however! It will only ever trust the server certificate we just made. So we get the trust we want, without the responsibility of maintaining and protecting a full blown CA!

Now let's look how to use these certificates and keys. First we need to make our browser use our CA certificate as a trusted authority. The options for importing can be found under:

  • Chrome: Settings -> Privacy and Security -> Security -> Manage Certificates -> Authorities -> Trust this certificate for identifying websites
  • Firefox: Preferences -> Privacy and Security -> Certificates -> View Certificates -> Authorities -> Import -> Trust this CA to identify websites

The import dialog in Chrome

Now let's spin up an HTTPS server in docker to make use of the certificate. For simplicity, we'll use nginx with a custom config in docker-compose. I'm going to show you the files to create, but you can also download off my gitlab repo to save time.

Testing with a local HTTPS server

Let's make our directory structure...

cd ..
mkdir conf.d html

In conf.d, create a file named web.conf with the following contents

server {
    listen 443 ssl;
    server_name host1.home.lan;
    ssl_certificate certs/server.crt;
    ssl_certificate_key certs/server.key;
    ssl_protocols TLSv1.2;
    ssl_ciphers HIGH:!aNULL:!MD5;

    root /usr/share/nginx/html;
}

This is a standard TLS configuration for nginx. The key points are that we tell nginx to use the server key and the server certificate that we created in our certs directory.

In html, create a file index.html with the followings contents

<html>
  <head><title>LAN TLS Test</title></head>
  <body><h1>LAN TLS Test</h1></body>
</html>

This is just some sample HTML that nginx will serve when we visit the main page.

Finally, in the base directory create a file name docker-compose.yml with the following contents

version: '3'
services:
  web:
    image: nginx:latest
    ports:
      - '8443:443'
    volumes:
      - ./conf.d:/etc/nginx/conf.d
      - ./certs:/etc/nginx/certs
      - ./html:/usr/share/nginx/html

This is a docker compose config file that will spin up an nginx image on our local host port 8443. The volumes section maps the certs, config, and html directories we just created to the appropriate places inside the nginx image.

Your directory structure should look like this

├── certs
│   ├── ca.key
│   ├── server.crt
│   ├── server.key
│   └── v3.ext
├── conf.d
│   └── web.conf
├── docker-compose.yml
└── html
    └── index.html

The final step before trying this out is making sure that our chosen host name, host1.home.lan is a valid DNS name. This is important because the browser will check that the host name matches the Common Name or one of the Subject Alternate Names in the certificate. If you have a DNS system set up for your LAN, you could put the name there. For simplicity in this example I'm just going to put the name in /etc/hosts.

# Add to /etc/hosts
192.168.0.10    host1.home.lan # Check against our real host in VM

Now, we just start the docker container and try it out!

docker-compose up web

Visit https://host1.home.lan:8443/ in your browser. If everything worked you should see a valid locked icon!

A valid local TLS certificate

Sadly, you will have to import the CA certificate into every browser you want the site to be trusted in. So, if you have many users (or browsers) on your LAN, this is still not a great option. But for a handful of computers on your home network it is an adequate solution.

Bonus - Certificate options

Now that we know how to create a certificate, let's look at some different types of host certificates we can make. The certificate we just made is the classic single host named certificate. This certificate will work for a host that matches the specified DNS name.

Multiple specific hosts in a single certificate

If we wanted to, using the Subject Alternate Name fields, we could create a single certificate that could be used by two or more hosts by adding those host names as DNS entries in the SAN field. The procedure is the same as above, but the v3.ext file would now look like this

[EXT]
subjectAltName=@alt_names
[alt_names]
DNS.0=host1.home.lan
DNS.1=host2.home.lan

The resulting server.key and server.crt could be used on both the host1 and host2 servers.

Wildcard certificates

If we wanted to create a certificate that can be reused on any named server on our LAN, we can create a wildcard certificate. To do this, we'd use *.home.lan as the common name and the single entry in the SAN field. The v3.ext file would look like this

[EXT]
subjectAltName=DNS.0:*.home.lan

The resulting server.key and server.crt could be used on any host on the home.lan domain. Obviously this lowers security in that, if the key and cert is stolen, and a DNS name can be obtained, a bad actor might add a server on the LAN that would now be automatically trusted by the browsers that have imported the CA certificate.

Use IP address(es) instead of domain name

All the examples up to now assume we've added /etc/hosts entries for the all the hosts we're creating certificates for, or have added the hostnames to our DNS servers. If we don't have a DNS server, or just don't want to bother with it, we can also make a certificate for a specific IP or a set of IPs. We just need to use one of the IPs for the common name and list all the IPs in the extension file. Here's what the v3.ext file would look like.

[EXT]
subjectAltName=@alt_names
[alt_names]
IP.0=192.168.0.42
IP.1=192.168.0.10

The resulting server.key and server.crt could then be used for servers on both the IPs listed. For example, going to https://192.168.0.42, in this case, would result in the page loading as a trusted site.

Automation

Now that we understand the certificate creation process and the types of certificates we can create, it makes sense to automate the steps. I've created a bash script that can do everything we've covered in this article.

Conclusion

To summarize, we've learned how the browser site trust system works. We've come up with a one-shot CA certificate creation process than can make a certificate for us without us having to maintain a Certificate Authority for all time. We've learned how to make our browser trust this CA. And finally, we've covered several types of certificates we might want to make for our custom situation.

Credits

Thumbnail photo by FLY:D on Unsplash