A lot of system administrators are faced with the challenge of obtaining TLS certificates for internal machines that are not exposed to the public. In these scenarios, a domain is mapped to a private IP address by a link-local DNS server. For example, when a user on the internal network accesses https://example.com, their browser connects to 172.16.0.1:443. HTTP-based challenges don’t work with the internal machines. Let’s encrypt cannot connect to the internal servers. However, there are a few solutions to obtain TLS certificates from a CA.

  • Use an internal CA to sign certificates or self-signed certificates. The disadvantage is that each user on every device needs to trust the custom certificate.
  • Use Let’s Encrypt (or any other CA) with a challenge that doesn’t require HTTP access to that machine. For example, the internal server could interact with a DNS service to pass a DNS-based challenge and obtain certificates.
  • It is also possible to use an ad-hoc solution with a public-facing server that handles the public requests to https://example.org. A system administrator performs the HTTP-based challenge on the public counterpart and copies the certificates to the internal machine. Although this is a very simple solution, it requires manual actions every two to three months when the certificates approach their expiry date.

There is another lesser-know solution, that leverages Let’s Encrypt accounts.

Concept

The key insight is that once an account proves eligibility for a certificate, e.g., via the HTTP challenge, Let’s Encrypt will mint multiple certificates for the same domain. The following sketch illustrates the idea.

Account sharing with surrogate server

The public surrogate server, reachable via https://example.com, requests a certificate for example.com and proves eligibility via the HTTP challenge (1). Certificate renewal can be automated such that there is always a valid certificate on the server. This could be used to host a public-facing website in place of the internal application. The surrogate server uses a user account, here xyz, to obtain the certificate.

Let’s Encrypt keeps track of which account passed which challenge. The internal server, hosting an internal application, uses the same credentials and therefore also the same account in the communication with Let’s Encrypt. The internal server can now request again a certificate for example.com. The eligibility has already been established and Let’s Encrypt mints the certificate right away.

What sounds like a bug at first, is covered by ACME specification: RFC 8555.

The “authorizations” array of the order SHOULD reflect all authorizations that the CA takes into account in deciding to issue, even if some authorizations were fulfilled in earlier orders or in pre-authorization transactions. For example, if a CA allows multiple orders to be fulfilled based on a single authorization transaction, then it SHOULD reflect that authorization in all of the orders.

The specification does not mandate this behavior, but Let’s Encrypt supports it.

Implementation steps

On the public-facing surrogate server

  1. Set up certbot in combination with a web server.
  2. Request a certificate for the domain in question.
  3. Automate certificate renewal, e.g., using certbot renew as a cron job.

On the internal server

  1. Set up certbot.
  2. Copy /etc/letsencrypt/accounts from the surrogate server to the internal.
  3. Request a certificate for the domain in question.
  4. Automate certificate renewal, e.g., using certbot renew as a cron job.

Done.