Tested with OpenBSD 6.4
httpd supports TLS 1.2 and works well with acme-client. In this example, relayd(8) only adds some HTTP headers to get higher grades from the following tests:
A+
Observatory by Mozilla
A+
SSL Labs by Qualys
A
CryptCheck
A+
Security Headers
+
HSTS Preload
100
Lighthouse by Google
There are some drawbacks:
Because relayd(8) is fronting httpd(8): REMOTE_ADDR
in
access.log
is always 127.0.0.1
. Here is a diff for httpd(8)
to include X-Forwarded-For
and X-Forwarded-Port
to the log.
Also httpd(8) doesn’t support gzip
compression for static files. You can use gzip
via FastCGI, if
needed.
httpd(8) listens on ports 80
and 8080
, serves plain HTTP,
redirects //www.tld
to //tld
and http://tld:80
to https://tld:443
.
relayd(8) listens on ports 443
and terminates TLS for IPv4 and
IPv6 addresses, acme-client(1) issues a certificate via Let’s
Encrypt, cron(8) runs acme-client(1) to check and renew the
certifictate.
In this example, TLD is romanzolotarev.com
, IPv4 address of the server is 46.23.88.178
and IPv6 is 2a03:6000:1015::178
.
https://romanzolotarev.com → relayd 46.23.88.178 :443 or relayd 2a03:6000:1015::178:443 → httpd 127.0.0.1 :8080 HTTP 200 OK https://www.romanzolotarev.com → relayd * :443 → httpd 127.0.0.1 :8080 HTTP 301 https://romanzolotarev.com http://romanzolotarev.com or http://www.romanzolotarev.com → httpd * :80 HTTP 301 https://romanzolotarev.com
acme-client(1) stores a challenge in /var/www/acme
directory,
Let’s Encrypt sends an HTTP request GET /.well-known/acme-challengs/*
,
and httpd(8) serves static files from that directory on such requests.
Note: httpd(8) is chrooted in /var/www/
, so httpd(8) sees it as /acme/
.
# > /etc/httpd.conf echo ' server "romanzolotarev.com" { listen on 127.0.0.1 port 8080 location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } } server "www.romanzolotarev.com" { listen on 127.0.0.1 port 8080 block return 301 "https://romanzolotarev.com$REQUEST_URI" } server "romanzolotarev.com" { alias "www.romanzolotarev.com" listen on * port 80 block return 301 "https://romanzolotarev.com$REQUEST_URI" } ' #
Verify the configuration, enable and restart httpd(8).
# httpd -n configuration OK # # rcctl enable httpd # rcctl restart httpd httpd (ok) #
relayd(8) listens on port 443
and relays all HTTP requests
to port 8080
to be served by httpd(8).
Must read before setting HTTP headers:
HSTS deployment recommendations
Content security policy
Feature policy
TLS configurations
# > /etc/relayd.conf echo ' ipv4="46.23.88.178" ipv6="2a03:6000:1015::178" table <local> { 127.0.0.1 } http protocol https { tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" match request header append "X-Forwarded-For" value "$REMOTE_ADDR" match request header append "X-Forwarded-Port" value "$REMOTE_PORT" match response header set "Content-Security-Policy" value "default-src 'none'; style-src 'self'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'" match response header set "Feature-Policy" value "camera 'none'; microphone 'none'" match response header set "Referrer-Policy" value "no-referrer" match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload" match response header set "X-Content-Type-Options" value "nosniff" match response header set "X-Frame-Options" value "deny" match response header set "X-XSS-Protection" value "1; mode=block" return error pass } relay wwwtls { listen on $ipv4 port 443 tls listen on $ipv6 port 443 tls protocol https forward to <local> port 8080 } ' #
relayd(8) loads a full-chain certificate for both IPv4 and IPv6
addresses from $address.crt
file and private key from
private/$address.key
from /etc/ssl
directory.
Generate a temporary key and certificate, then create symbolic links for IPv4 and IPv6 addresses. Later that key and certificate will be replaced by acme-client(1).
# mkdir -p -m 0700 /etc/ssl/private # # openssl req -x509 -newkey rsa:4096 \ -days 365 -nodes \ -subj '/CN=romanzolotarev.com' \ -keyout /etc/ssl/private/romanzolotarev.com.key \ -out /etc/ssl/romanzolotarev.com.pem Generating a 4096 bit RSA private key .................................................++ ....................................................................++ writing new private key to '/etc/ssl/private/romanzolotarev.com.key' ----- # # ln -fs /etc/ssl/private/{romanzolotarev.com,46.23.88.178}.key # ln -fs /etc/ssl/private/{romanzolotarev.com,2a03:6000:1015::178}.key # ln -fs /etc/ssl/{romanzolotarev.com.pem,46.23.88.178.crt} # ln -fs /etc/ssl/{romanzolotarev.com.pem,2a03:6000:1015::178.crt} # # chmod 0600 /etc/ssl/private/*.key #
Verify the configuration, enable and restart relayd(8).
# relayd -n configuration OK # # rcctl enable relayd # rcctl restart relayd relayd (ok) #
acme-client(1) generates an account key letsencrypt.key
, a domain
key romanzolotarev.com.key
and stores them in /etc/ssl/private
, stores
challenges in /var/www/acme
directory, a cerficifate in
/etc/ssl/romanzolotarev.com.crt
(not needed for this setup), a full-chain
cerficifate in /etc/ssl/romanzolotarev.com.pem
(needed for relayd).
# > /etc/acme-client.conf echo ' authority letsencrypt { api url "https://acme-v01.api.letsencrypt.org/directory" account key "/etc/ssl/private/letsencrypt.key" } domain romanzolotarev.com { alternative names { www.romanzolotarev.com } domain key "/etc/ssl/private/romanzolotarev.com.key" domain certificate "/etc/ssl/romanzolotarev.com.crt" domain full chain certificate "/etc/ssl/romanzolotarev.com.pem" sign with "letsencrypt" } ' #
Remove the temporary cerficifate and keys, if any. Create the directory for challenges.
# rm -f /etc/ssl/romanzolotarev.com.pem # rm -f /etc/ssl/romanzolotarev.com.crt # rm -f /etc/ssl/private/romanzolotarev.com.key # rm -f /etc/ssl/private/letsencrypt.key # # mkdir -p -m 755 /var/www/acme #
Verify the configuration, run acme-client(1), and reload relayd(8).
# acme-client -n romanzolotarev.com authority letsencrypt { api url "https://acme-v01.api.letsencrypt.org/directory" account key "/etc/ssl/private/letsencrypt.key" } domain romanzolotarev.com { domain key "/etc/ssl/private/romanzolotarev.com.key" domain certificate "/etc/ssl/romanzolotarev.com.crt" domain full chain certificate "/etc/ssl/romanzolotarev.com.pem" sign with "letsencrypt" } # # acme-client -vFAD romanzolotarev.com acme-client: /etc/ssl/private/letsencrypt.key: generated RSA account key acme-client: /etc/ssl/private/romanzolotarev.com.key: generated RSA domain key acme-client: https://acme-v01.api.letsencrypt.org/directory: directories acme-client: acme-v01.api.letsencrypt.org: DNS: 23.15.57.150 acme-client: https://acme-v01.api.letsencrypt.org/acme/new-reg: new-reg acme-client: https://acme-v01.api.letsencrypt.org/acme/new-authz: req-auth: romanzolotarev.com acme-client: /var/www/acme/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: created acme-client: https://acme-v01.api.letsencrypt.org/acme/challenge/yyyyyyyyyyy_yyyyyyyyyyyyyyyyy-yyyyyyyyyyyyy/yyyyyyyyyyy: challenge acme-client: https://acme-v01.api.letsencrypt.org/acme/challenge/yyyyyyyyyyy_yyyyyyyyyyyyyyyyy-yyyyyyyyyyyyy/yyyyyyyyyyy: status acme-client: https://acme-v01.api.letsencrypt.org/acme/new-cert: certificate acme-client: http://cert.int-x3.letsencrypt.org/: full chain acme-client: cert.int-x3.letsencrypt.org: DNS: 23.13.65.208 acme-client: /etc/ssl/romanzolotarev.com.crt: created acme-client: /etc/ssl/romanzolotarev.com.pem: created # # rcctl reload relayd relayd(ok) #
Schedule a new crontab to check and renew the certificate.
# echo '0 0 * * * acme-client romanzolotarev.com && rcctl reload relayd' | crontab - #