Tag Archives: acme.sh

Proxmox VE and Let’s Encrypt with DNS-01 Validation

One of the appealing reasons for using Proxmox VE as your hypervisor is that one can configure their system to obtain a TLS certificate for https from Let’s Encrypt on a regular basis.

The Environment

At the time of writing, I am running Proxmox VE version 7.2-4. The name of the node for this article will be pve.
I have a dynamic DNS zone (i.e. acme.example.net) running BIND for the purposes of enabling ACME clients (acme.sh) to update the dynamic zone with the appropriate TXT record. A CNAME will be created in the “top-level” zone (example.net) such that querying _acme-challenge.pve.example.net will be answered by _acme-challenge.pve.acme.example.net.

Configuration

In PVE, go to Datacenter > ACME and then click Add under Accounts to register an ACME account.

The next step is to add a Challenge Plugin. On the same screen click Add under Challenge Plugins.

Plugin ID: nsupdate
Validation Delay: 30 (default)
DNS API: nsupdate (RFC 2136)
NSUPDATE_KEY=/var/lib/pve/nsupdate.key
NSUPDATE_SERVER=acme.ns.example.net

Since I am using nsupdate as the DNS API, I generate a key locally:

$ ddns-confgen -a hmac-sha256 -k pve.example.net. -q > pve.key 

Transfer the key to PVE to the location specified in NSUPDATE_KEY. Below are the user/group and permissions for reference.

# ls -l /var/lib/pve/nsupdate.key
-rw-r--r-- 1 root root 128 Jun 21 19:43 /var/lib/pve/nsupdate.key

Now go to the node itself under Datacenter. Go to System > Certificates. Under ACME, click Add.
Select DNS as the Challenge Type, select nsupdate as the plugin, and enter the PVE host’s domain.

Since we have delegated the actual records to another DNS zone, we need to make one small change to the (PVE) node’s configuration. Under the DNS Validation through CNAME Alias of the documentation:

set the alias property in the Proxmox VE node configuration file

https://pve.proxmox.com/wiki/Certificate_Management#sysadmin_certs_acme_dns_challenge

To do that, I ssh-ed into the node (as root), opened /etc/pve/local/config in nano, and added alias=pve.acme.example.net to the end of the line that has the domain (in my case, it was the line that started with acmedomain0)

# cat /etc/pve/local/config
acme: account=default
acmedomain0: pve.example.net,plugin=nsupdate,alias=pve.acme.example.net

Save (CTRL+O) and Exit (CTRL+X)

Back in the web interface, in the Certificates screen (Datacenter > Your node (pve) > System > Certificates) you should be able to select the domain and click Order Certificates Now.

At this point PVE should be able to create a TXT _acme-challenge record in the (delegated) dynamic DNS zone, Let’s Encrypt should be able to validate it, and we should be able to get an TLS certificate for https.

Delegate ACME challenges to another nameserver

I have been using Let’s Encrypt’s DNS-01 challenge to prove that I control DNS for domain names. However, obtaining certificates and renewing them have been a manual process where I added scripts to getssl to tell me the TXT record(s) to add for that domain. I did it this way because I did not like the idea of giving a script full capability (at the time) to play with my DNS records. I do not remember where, but I learned that we can CNAME TXT records which means that a TXT record can be a CNAME pointing at another record that can be updated. Neat.

Plan

  • Set up a BIND 9 server
    • Set up a dynamic zone for acme.example.net
    • Generate ddns keys for servers that host a service that uses an SSL certificate
  • Update DNS
    • Add A and NS records such that acme.example.net queries are handled by another nameserver
    • Add CNAME records for _acme-challenge.foo.example.net to point at _acme-challenge.foo.acme.example.net
  • Request certificates for a given set of domains

Most of what I did was influenced by Dan Langille’s post at https://dan.langille.org/2019/02/01/acme-domain-alias-mode/.

BIND 9

Install

On Debian, installing BIND 9 was done by executing sudo apt install bind9.

Disable recursion

One of the things that we need to do is to disable recursion such that our nameserver is not an open resolver. Recursion is disabled by adding the following to the options block in /etc/bind/named.conf.options

recursion no;

Set up a dynamic zone

Copy /etc/bind/db.empty to /var/lib/bind/db.acme.example.net and edit accordingly.

$TTL	86400
@	IN	SOA	a.ns.example.net. no-reply.acme.example.net. (
		    2020010700 	; Serial
			  43200 	; Refresh
			   7200	; Retry
			2419200	; Expire
			  86400 )	; Negative Cache TTL
;
@	IN	NS	a.ns.example.net.

db.acme.example.net will be light since it is a dynamic zone.

Update /etc/bind/named.conf.local with the dynamic zone

zone "acme.example.net." {
	type master;
	file "/var/lib/bind/db.acme.example.net";
};

ddns key

We will now generate a dynamic DNS key that will allow our ACME client of choice to dynamically update the dynamic zone we have created.

On Arch Linux, I used the following ddns-confgen invocation to generate a ddns key

ddns-confgen -a hmac-sha512 -k one.example.net. -q > one.example.net.key

The contents of one.example.net.key

key "one.example.net." {
	algorithm hmac-sha512;
	secret "iXdUnXCbxJqcr3rgxYVdvHVqLmJkHW6mNZEDsXwDXtvxIfVmoABHWjN4ko4+Rz2uEmgOk+IFfqgEVGrAYTWaIA==";
};

Now that we have a ddns key, we will need to make BIND 9 aware of it.

In /etc/bind/named.conf, we will add an include for a new file at /etc/bind/named.conf.keys

include "/etc/bind/named.conf.keys";

The content of /etc/bind/named.conf.keys will be the contents of one.example.net.key. Setting it up this way allows us to add named.conf.keys to .gitignore if we’re using configuration management such as Ansible.

Last but not least, we will update the zone configuration in /etc/bind/named.conf.local with an update-policy that says this key (one.example.net.) is permitted to update this zone:

zone "acme.example.net." {
	type master;
	file "/var/lib/bind/db.acme.example.net";
	update-policy {
		grant one.example.net. zonesub TXT;
	};
};

With the above configuration, the one.example.net. key would be able to update any TXT record within the acme.example.net zone. If desired, we could restrict the key to a domain such as assets.example.net, we would replace the grant line with the following:

grant one.example.net. name _acme-challenge.assets.acme.example.net. TXT;

Add DNS records

To delegate the _acme-challenge record to a subdomain, I added the following DNS records to the nameserver serving example.net:

a.ns IN A 192.168.26.42
acme IN NS a.ns.example.net.

Now for each domain I want to delegate the _acme-challenge record to, I added a CNAME like the following:

_acme-challenge.subdomain IN CNAME _acme-challenge.subdomain.acme.mydomain.net.

Request Certificates

At this point, we upload one.example.net.key to our box. I stash mine as /root/nsupdate.key. Then I use the following acme.sh invocation to request a certificate for one.example.net.
NSUPDATE_SERVER and NSUPDATE_KEY are environment variables to clue acme.sh to the name server to update and the key to use.

# NSUPDATE_SERVER=a.ns.example.net NSUPDATE_KEY=/root/nsupdate.key acme.sh --issue --dns dns_nsupdate -d one.example.net --challenge-alias one.acme.example.net --server letsencrypt --reloadcmd 'systemctl reload nginx'

With acme.sh we could use letsencrypt_test as the --server so that we can test to make sure things are working as expected.