The “Problem”
It was annoying to keep telling my browser “yeah, yeah it’s OK to proceed to this unsafe site” whenever I wanted to access the web UI on my pfSense device. An annoyance class problem but still fun to solve for robust future laziness aka functional auto-renew!
I’ll cover the high level idea and some of the implementation detail I found clutch, but this is not at all meant to be an exhaustive end-to-end HowTo. That said, if you’re stuck where I was this might help you.
Ingredients and my version of them
- firewall hardware - Netgate 4100
- firewall software - pfSense 23.09.1-RELEASE (amd64)
- Let’s Encrypt client - pfsense package
acme v0.7.5
- external, Internet resolvable DNS - AWS Route53
- LAN-hosted webserver - Apache2 on Ubuntu 22.04.4 LTS
- Firewall NAT rules
The Basic Idea
In order for Let’s Encrypt to issue you a certificate, its servers have to be able to access the device you want a certificate via HTTP. Since I don’t want to expose the web server on my firewall to the Internet, this presents a problem.
The solve was to have a LAN-based web host answer the Let’s Encrypt response to the pfSense acme
client’s call via Webroot FTP (not actually FTP) method configured in Services > Acme certificates
.
Making it work
First, understand that my solution lies at the intersection of my interest and laziness. There is likely a more secure, more whizbang way to do this. I stopped at “works, and seems reasonably secure”, where my sense of ‘secure’ is, as mentioned, somewhat subject to my laziness.
Next, let’s understand the Webroot FTP method of answering a Let’s Encrypt (“LE”) query.
- The web server you want to issue a certificate to, runs a client that asks LE’s servers for a certificate
acme
opens an SSH session to your on-LAN web server and puts the challenge response it generated there, to an absolute (not relative) directory you specify, that matches where it told LE to look for it relative to the webroot- LE’s servers do a lookup in public DNS for the name of the host you said you wanted a certificate for
- LE’s servers make an HTTP request of that host, by name, for the file the
acme
client told it to look for (the challenge response file) - Configured robustly, this all happens again every 60 days without you having to think about it again
Stuff you need to set up
- A web server on your LAN aka
lanwebhost
- Firewall rules that setup a NAT from your external, routable IP (aka
externalIP
) and ports80
to port80
on the LAN interface of the web server aka - A local account on the web server that can be SSHed to by password (I know, I know. Don’t expose port
22
on this host to the Interwebz, ok? Ok.), akaacmeuser
- Create external DNS host entries that specify the name of the host you want to configure, and if necessary, any intervening subdomain(s)
- An Acme account key in
Services > Acme certificates > Account keys
- Configure the
acme
client to useWebroot FTP
as the method for the challenge response
Examples of each
Web server: Straightforward Apache2 setup on Ubuntu 22.04.4 LTS that - critically in my experience - has a
VirtualHost
config forhost.yourdomain.tld
(if subdomain in play,host.sudomain.yourdomain.tld
), akalanwebhost
lanwebhost
also must allow SSH by password
An externally routable IP, aka your ISP router’s public interface, aka
externalIP
pfSense rules that NAT your
externalIP:80
tolanwebhost:80
A role account (it’s only used for this one thing) on
lanwebhost
, akaacmeuser
- a user group (
/etc/groups
) for this user, akaacmeusergroup
- Don’t Forget: You need to set the permissions on the
sftp
target folder correctly for this user to be able to write files there, e.g. making the folder group-writeable byacmeuser
’s group ID, e.g.chown acmeusergroup /www/default/htdocs/.well-known/acme-challenge/; chmod g+rwx /www/default/htdocs/.well-known/acme-challenge/
- a user group (
Externally resolvable hostname for your
externalIP
that matches the name you put in the certificate request, akahost.yourdomain.com
orhost.bar.yourdomain.com
if you have fancy internal subdomain.- If you use a subdomain, you’ll need your external DNS to have an entry for the subdomain and for the host in that subdomain. e.g subdomain
bar.yourdomain.tld
which resolves toexternalIP
, andhost.bar.yourdomain.tld
which also resolves to yourexternalIP
- I use a dynamic DNS client on the pfSense to register
externalIP
in AWS’s Route53. Setting this up is beyond the scope of this guide; there are good guides out there, I used ’em!
- If you use a subdomain, you’ll need your external DNS to have an entry for the subdomain and for the host in that subdomain. e.g subdomain
You can give your account key any name you want, for example
moo
Likewise, you can give your Acme config any name you want, for example,
foo
The configuration details of the certificate issuing setup is where names start to matter.
Domainname
must be the externally resolvable hostname you want a certificate issued in the name of / forServer
has to be the addressing by name or IP that your pfSense device can access yourlanwebhost
at, e.g.labwebhost.lan
or192.168.0.2
or whateverUsername
has to be the name ofacmeuser
account created for this- for maximum laziness and less security, you could use your personal account
- whatever account, see Don’t Forget: above and set folder perms so this account can write to the
sftp
target folder
Password
probably you can guess has to be the password foracmeuser
Folder
is the absolute path name - the full path name starting from the root directory ,/
on yourlanwebhost
- where theacme
client will SSH the challenge response to, akasftp
target folder
Troubleshooting
- pfSense will give you UI output that directs you to a log file if things don’t go right. There are good clues there.
- On
lanwebhost
looking at the access log will help determine if your pfSense host is able to ssh into it ;sudo tail -f /var/log/auth.log
- On
lanwebhost
, looking at the HTTP server access logs helps a bunch;sudo tail -f /var/log/apache2/access.log
forDefault
andsudo tail -f /var/log/apache2/other_vhosts_access.log
forVirtualHost
s- This was the most clutch for me, reminding me that I needed a
VirtualHost
entry on the server that matched the hostname Let’s Encrypt was trying to access. Without thislanwebhost
was defaulting to the default host config, which, even though the absolute path to the challenge response file was the same for both default host and theVirtualHost
host.bar.youdomain.tld
, the default server would not respond with the file. Once there was a properly namedVirtualHost
, life was good.
- This was the most clutch for me, reminding me that I needed a
Hopefully at this point, your browser has stopped complaining, and there’s no more of this kind of thing, below!