Let's Encrypt (LE) has been a popular choice to get certs for public websites. Because it's free and automated. But how to get certs for private websites, which are common in company's intranet?

Problem

  • There's a web app in your company's intranet.
  • The web app has a fully qualified domain name (FQDN), e.g. foo.example.com, not an internal one like foo.internal.
  • It only resolves to a private IP behind VPN. Therefore, it's inaccessible without a valid VPN.
  • You want to add an extra layer of security by enabling HTTPS.

How to get a cert for it? And how to automate it and get it for free?

You may be wondering why not use the company's wild card cert (assuming it has one), i.e. *.example.com. Because wild card cert only supports one level of subdomain matching. If you have cert for *.example.com, it can be used for foo.example.com. But what about foo.bar.example.com? The problem remains.

Solution

One quick and dirty way is to geneate a self-signed cert. But this will make browsers give a cert warning because it's not trusted. You don't want that.

How to use LE when the site is inaccessible publicly? There's a simple and elegant solution.

LE relies on ACME protocal to verify domain ownership. There're a few types of challenges that ACME uses to verify domain ownership. The most common one is HTTP Challenge. But it's not applicable in this case because the intranet site's port 80 is inaccessible publicly. Similarly, TLS SNI Challenge can't be used because the intranet site's port 443 is inaccessible publicly either. Out-of-Band Challenge isn't really automated by definition. The only option left is DNS Challenge. DNS Challenge works by provisioning a TXT record containing a designated value for the domain. Put simply,

  1. A designated value is generated by LE server
  2. A TXT record containing the designated value has to be present on the domain
  3. LE server queries the domain's TXT records
  4. LE server verifies that the contents of one of the TXT records matches the designated value

If all of the above steps succeed, the validation is successful. Cert is generated by LE.

The advantage of this challenge is that it only requires provisioning a TXT record and doesn't require a server to be publicly accessible on either port 80 or 443, which fits the intranet site case perfectly.

Implementation

How to automate DNS Challenge and get a cert depend on your existing infra automation (you're not manually creating your infra, are you?). I use Terraform with AWS provider. Below is an example to get a cert for foo.example.com using DNS Challenge and load it to ELB. Credits to the Terraform ACME provider

# code for acme registration and private key creation is omitted

resource "acme_certificate" "certificate" {
  server_url               = "${var.acme_server_url}"
  account_key_pem          = "${tls_private_key.private_key.private_key_pem}"
  common_name              = "foo.example.com"
  must_staple = true

  dns_challenge {
    provider = "route53"
  }

  registration_url = "${acme_registration.reg.id}"
}

resource "aws_iam_server_certificate" "your_cert" {
  name_prefix       = "foo-example-cert"
  certificate_body  = "${acme_certificate.certificate.certificate_pem}"
  certificate_chain = "${acme_certificate.certificate.issuer_pem}"
  private_key       = "${acme_certificate.certificate.private_key_pem}"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_elb" "foo" {
  # other configs for the elb are omitted

  listener {
    instance_port      = 80
    instance_protocol  = "http"
    lb_port            = 443
    lb_protocol        = "https"
    ssl_certificate_id = "${aws_iam_server_certificate.your_cert.arn}"
  }
}

Of course, this assumes that the domain foo.example.com is managed by Route 53.