OpenBSD acme-client For Let's Encrypt Certificates
Tags: openbsd, tls, security, letsencrypt
Published: 2022-03-04 17:08:11

## Summary

### OpenBSD's acme-client

[`acme-client`](https://man.openbsd.org/acme-client.1) is the default Automatic Certificate Management Environment (ACME) client on [OpenBSD](https://www.openbsd.org/), installed at the same time when the OS is.


To configure [acme-client.conf](https://man.openbsd.org/acme-client.conf.5) is easy and enables us to get certificates with [Let's Encrypt](https://letsencrypt.org/) or another.


The basic usage is:

```console
# # 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](https://certbot.eff.org/) by [EFF (Electronic Frontier Foundation)](https://www.eff.org/), a very popular client. Although we can get it via `pkg_add certbot`, there was [sometimes a problem around permissions](https://github.com/OpenSMTPD/OpenSMTPD/issues/785) on OpenBSD when renewing the certificate.


### Environment

- OS: OpenBSD [7.0](https://www.openbsd.org/70.html)
- ACME client: OpenBSD acme-client
- Certificate Authority: Let's Encrypt
- Web server: OpenBSD [httpd](https://man.openbsd.org/httpd.8)

## How to use

### Configure `acme-client.conf`

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

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

There are clear examples:

```conf
#
# $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:me@example.com"
}

authority buypass-test {
	api url "https://api.test4.buypass.no/acme/directory"
	account key "/etc/acme/buypass-test-privkey.pem"
	contact "mailto:me@example.com"
}

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:

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

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

Then edit:

```console
$ doas nvim /etc/acme-client.conf
```

like below:

```diff
- 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:

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

Well, you may also find the example of [`httpd.conf`](https://man.openbsd.org/httpd.conf.5) in `/etc/examples/`.


Of course, it has to be started:

```console
$ # 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:

```console
$ doas acme-client -v your.domain
```

Well, `-v` is for verbose and optional.

There are other methods. How to update it:

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

Moreover, how to revoke it:

```console
$ 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`](https://man.openbsd.org/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](https://letsencrypt.org/docs/rate-limits/) on it.

### Side story: The history and the changelog

acme-client first appeared in [6.1](https://www.openbsd.org/61.html).


In [6.5](https://www.openbsd.org/65.html) or below (to 6.1), you had to use more options to get certificate:

```console
$ doas acme-client -ADv your.domain
```

In [6.6](https://www.openbsd.org/66.html), 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:

```console
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:

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

Then restart httpd:

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

It must be fixed.

```console
# acme-client -v your.domain
```

I hope you meet success like this:

```console
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`.

```diff
  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🕊