OpenBSD acme-client For Let's Encrypt Certificates

created
( modified )
@nabbisen

Summary

OpenBSD’s acme-client

acme-client is the default Automatic Certificate Management Environment (ACME) client on OpenBSD, installed at the same time when the OS is.

To configure acme-client.conf is easy and enables us to get certificates with Let’s Encrypt or another.

The basic usage is:

# # configure
# nvim /etc/acme-client.conf

# # get or update (for renewal) certificate
# acme-client <domain>

# # revoke certificate
# acme-client -r <domain>

How about CertBot

Besides, we know there is another option. Yes, CertBot by EFF (Electronic Frontier Foundation), a very popular client. Although we can get it via pkg_add certbot, there was sometimes a problem around permissions on OpenBSD when renewing the certificate.

Environment

  • OS: OpenBSD 7.0
  • ACME client: OpenBSD acme-client
  • Certificate Authority: Let’s Encrypt
  • Web server: OpenBSD httpd

How to use

Configure acme-client.conf

Well, let’s see the conf file which is officially provided by OpenBSD.

$ cat /etc/examples/acme-client.conf

There are clear examples:

#
# $OpenBSD: acme-client.conf,v 1.4 2020/09/17 09:13:06 florian Exp $
#
authority letsencrypt {
	api url "https://acme-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
	api url "https://acme-staging-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

authority buypass {
	api url "https://api.buypass.com/acme/directory"
	account key "/etc/acme/buypass-privkey.pem"
	contact "mailto:[email protected]"
}

authority buypass-test {
	api url "https://api.test4.buypass.no/acme/directory"
	account key "/etc/acme/buypass-test-privkey.pem"
	contact "mailto:[email protected]"
}

domain example.com {
	alternative names { secure.example.com }
	domain key "/etc/ssl/private/example.com.key"
	domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
	sign with letsencrypt
}

Let’s copy it:

$ # well, you can see there is none by default
$ ls /etc/acme-client*

$ doas cp -p /etc/examples/acme-client.conf /etc/

Then edit:

$ doas nvim /etc/acme-client.conf

like below:

- domain example.com {
- 	alternative names { secure.example.com }
- 	domain key "/etc/ssl/private/example.com.key"
- 	domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
- 	sign with letsencrypt
- }
+ domain your.domain {    
+ 	#alternative names { secure.example.com }    
+ 	domain key "/etc/ssl/private/your.domain.key"    
+ 	domain full chain certificate "/etc/ssl/your.domain.fullchain.pem"    
+ 	sign with letsencrypt    
+ }

OK. your.domain is now ready for “sign with letsencrypt”.

Start OpenBSD httpd as web server

Be sure to httpd listens on egress:

server "your.domain" {
	listen on egress port 80
	# ...
}

Well, you may also find the example of httpd.conf in /etc/examples/.

Of course, it has to be started:

$ # case disabled:
$ doas rcctl start -f httpd
$ # case enabled:
$ doas rcctl restart httpd

Request certificate

Just run the single command below, and you will get the sweet certificate:

$ doas acme-client -v your.domain

Well, -v is for verbose and optional.

There are other methods. How to update it:

$ # the same command above
$ doas acme-client your.domain

Moreover, how to revoke it:

$ doas acme-client -r your.domain

Notes

Periodical update is required

Remember Let’s Encrypt certificate lasts only 90 days. You have to update it periodically.

One way to do so is to use cron.

Let’s Encrypt has their Rate Limits

Be careful about that Let’s Encrypt has several rate limits.

For example,

  • “Certificates per Registered Domain”. It is 50 per week.

Also,

  • “Duplicate Certificate”. You can renew the same certificate up to 4 times (totally 5 times) per week.

Here is their documentation on it.

Side story: The history and the changelog

acme-client first appeared in 6.1.

In 6.5 or below (to 6.1), you had to use more options to get certificate:

$ doas acme-client -ADv your.domain

In 6.6, acme-client became to able to communicate with the v02 Let’s Encrypt API. Therefore, -A and -D have not been necessary any more. Nice improvement.

Troubleshooting: When the challenge failed

You might sometimes see such errors and, sadly, fail:

acme-client: /etc/ssl/private/your.domain: generated RSA domain key
acme-client: https://acme-v02.api.letsencrypt.org/directory: directories
acme-client: acme-v02.api.letsencrypt.org: DNS: xxx.xxx.xxx.xxx
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84303399960
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84303399960/MQGXJQ, status: 0
acme-client: /var/www/acme/(...): created
acme-client: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84303399960/MQGXJQ: challenge
acme-client: order.status 0
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84303399960
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84303399960/MQGXJQ, status: -1
acme-client: order.status -1
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84303399960
acme-client: Invalid response from http://your.domain/.well-known/acme-challenge/(...) [2606:4700:3031::6815:3ae6]: \\"\\u003c!DOCTYPE html\\u003e\\\\n\\u003chtml\\u003e\\\\n\\u003chead\\u003e\\\\n\\u003cmeta charset=\\\\\\"utf-8\\\\\\"\\u003e\\\\n\\u003ctitle\\u003e404 Not Found\\u003c/title\\u003e\\\\n\\u003cstyle type=\\\\\\"text/css\\\\\\"\\u003e\\u003c!--\\\\nbody { background-\\"
acme-client: bad exit: netproc(87417): 1

It is 404 aka page-is-missing error. In other cases, it may 403 aka access-is-denied.

They are because your web server returned “bad response”. Let’s mitigate it by properly mapping response challenges:

  server "your.domain" {
  	listen on egress port 80
+ 	# acme-client
+ 	location "/.well-known/acme-challenge/*" {
+ 		root "/acme"
+ 		request strip 2
+ 	}
  	# ...
  }

Then restart httpd:

$ # case disabled:
$ doas rcctl -f restart httpd
$ # case enabled:
$ doas rcctl restart httpd

It must be fixed.

# acme-client -v your.domain

I hope you meet success like this:

acme-client: https://acme-v02.api.letsencrypt.org/directory: directories
acme-client: acme-v02.api.letsencrypt.org: DNS: xxx.xxx.xxx.xxx
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84304202350
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84304202350/NVmlRA, status: 0
acme-client: /var/www/acme/(...): created
acme-client: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84304202350/NVmlRA: challenge
acme-client: order.status 0
acme-client: dochngreq: https://acme-v02.api.letsencrypt.org/acme/authz-v3/84304202350
acme-client: challenge, token: (...), uri: https://acme-v02.api.letsencrypt.org/acme/chall-v3/84304202350/NVmlRA, status: 2
acme-client: order.status 1
acme-client: https://acme-v02.api.letsencrypt.org/acme/finalize/58118741/68735636600: certificate
acme-client: order.status 3
acme-client: https://acme-v02.api.letsencrypt.org/acme/cert/(...): certificate
acme-client: /etc/ssl/your.domain: created

Conclusion

You have got the Let’s Encrypt certificate! All what to do is add it to httpd.conf.

  server "your.domain" {
  	listen on * port 80
  	# acme-client
  	location "/.well-known/acme-challenge/*" {
  		root "/acme"
  		request strip 2
  	}
+ 	block return 301 "https://$SERVER_NAME$REQUEST_URI"
  }
+ server "your.domain" {
+ 	listen on * tls port 443
+ 	tls {
+ 		certificate	"/etc/ssl/your.domain.fullchain.pem"
+ 		key		"/etc/ssl/private/your.domain.key"
+ 	}
+ 	# acme-client
+ 	location "/.well-known/acme-challenge/*" {
+ 		root "/acme"
+ 		request strip 2
+ 	}
+ 	# ...
+ }

Here, I replaced egress with * to apply also to lo. It is optional.

Your TLS connection based on encryption aka HTTPS is completed.

Happy connecting securely🕊


Comments or feedbacks are welcomed and appreciated.