Berkeley smorgasbord — part II
OpenBSD makes life pretty easy when it comes to securing your web-app. In this short post, we will go through the steps to:
- secure traffic to ‘foobar.com’ (our web-app) and ‘cdn.foobar.com’ (our assets host)
- still just have plaintext traffic to ‘barbaz.com’
- make sure our TLS certificate gets updated automatically via ‘cron’
We will be building upon our previous post for those who came to this all just now. It’s assumed that the state-of-the-server is in place from this previous post, meaning, our ‘foobar’ web-app is all humming away via ‘relayd,’ ‘httpd,’ Erlang/OTP, etc.
It is also assumed that ‘letsencrypt.org’ CAA
DNS records are in place, that port 443 is open
on your box, and that doas
is being employed for
root-access.
Order of operations
It’s important to do several of these steps in a particular order. Otherwise, some chicken/egg troubles will stop you in your tracks. First, as our normal user, ‘joe,’ let’s back up our ‘relayd.conf’ and ‘httpd.conf’ files:
cp /etc/relayd.conf ~/relayd.conf.bak
cp /etc/httpd.conf ~/httpd.conf.bak
Next, we need to improve upon ‘httpd.conf’ and get
it in its final state that can handle ‘https,’
answer acme-client
challenge questions over
plaintext, etc.
Here’s the improved file, that we’ll also just
keep in ~
for simplicity:
‘httpd.conf.master’
int_ip="127.0.0.1"
server "foobar.com" {
alias "www.foobar.com"
alias "cdn.foobar.com"
listen on $int_ip port 8080
# make /var/www/acme available
location "/.well-known/acme-challenge/*" {
# /var/www/acme
root "/acme"
# remove .well-known and acme-challenge from the path
# before looking for the file in /var/www/acme
request strip 2
}
# redirect all other non-acme-client requests
location "/*" {
block return 301 "https://$SERVER_NAME$REQUEST_URI"
}
}
server "www.foobar.com" {
listen on $int_ip port 8443
block return 301 "https://foobar.com$REQUEST_URI"
}
server "cdn.foobar.com" {
listen on $int_ip port 8001
location "/assets/*" {
root "/htdocs/foobar/priv/assets"
request strip 1
}
}
server "barbaz.com" {
listen on $int_ip port 8080
location "/robots.txt" { root "/htdocs/local" }
root "/htdocs/barbaz.com"
}
server "www.barbaz.com" {
listen on $int_ip port 8080
block return 301 "http://barbaz.com$REQUEST_URI"
}
This file is ready to be used in conjunction with
acme-client
— and we don’t have to change
anything in ‘relayd.conf’ for this to all work.
But before we copy over our new ‘httpd’
configuration, it’s time to do a little prep-work
before using the acme-client
tool. First, make
sure the apt directories are in place:
doas mkdir -p -m 700 /etc/ssl/private # may be there
doas mkdir -p -m 755 /var/www/acme # may be there
Let’s now configure ‘/etc/acme-client.conf’ to our liking:
‘acme-client.conf’
authority letsencrypt {
api url "https://acme-v01.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
authority letsencrypt-staging {
api url "https://acme-staging.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-staging-privkey.pem"
}
domain foobar.com {
alternative names { www.foobar.com cdn.foobar.com }
domain key "/etc/ssl/private/foobar.com.key"
domain certificate "/etc/ssl/foobar.com.crt"
domain full chain certificate "/etc/ssl/foobar.com.fullchain.pem"
sign with letsencrypt
}
Now although ‘relayd’ isn’t ready to serve our
web-app over TLS, that doesn’t mean we can’t
get our certificates with acme-client
. We can,
in fact.
For a brief moment, we can expose our plaintext domains and resolve each ‘well-known/acme-challenge’ which puts our certificates in place. Then, we just put our old ‘httpd.conf’ back in place while we rework the rest of our OpenBSD server configurations.
Follow along with some care:
doas cp ~/httpd.conf.master /etc/httpd.conf
doas su
root> httpd -n
root> rcctl reload httpd
root> acme-client -vFAD foobar.com
root> exit
doas cp ~/httpd.conf.bak /etc/httpd.conf
doas httpd -n
doas rcctl reload httpd
So, what did we just do? We briefly took our web-app out of service; got our security certificates (see ‘/etc/ssl’); then brought back our web-app and ‘httpd’ service as it once was.
Hacking for service
There’s a bit of hacking needed to get ‘relayd’ to work with TLS and our web-app. But there’s nothing to fear. The complications aren’t too egregious.
First, let’s create a work-in-progress
‘relayd.conf’ in our ~
directory, like we did
with ‘httpd.conf.master.’ Here’s the new file:
‘relayd.conf.master’
relayd_addr="0.0.0.0"
plaintext_port="80"
app_port="8000"
table <app_hosts> { 127.0.0.1 }
cdn_port="8001"
table <cdn_hosts> { 127.0.0.1 }
web_port="8080"
table <web_hosts> { 127.0.0.1 }
tls_web_port="8443"
table <tls_web_hosts> { 127.0.0.1 }
log state changes
log host checks
log connection errors
http protocol "httpfilter" {
match header append "X-Forwarded-For" \
value "$REMOTE_ADDR"
match header append "X-Forwarded-By" \
value "$REMOTE_ADDR:$SERVER_PORT"
match header set "Keep-Alive" value "$TIMEOUT"
### TCP performance options
tcp { nodelay, sack, socket buffer 65536, backlog 100 }
### Return HTTP/HTML error pages
return error
}
http protocol "httpsfilter" {
match header append "X-Forwarded-For" \
value "$REMOTE_ADDR"
match header append "X-Forwarded-By" \
value "$REMOTE_ADDR:$SERVER_PORT"
match header set "Keep-Alive" value "$TIMEOUT"
match response header remove "Server"
match response header set "X-Frame-Options" \
value "SAMEORIGIN"
match response header set "X-XSS-Protection" \
value "1; mode=block"
match response header set "X-Content-Type-Options" \
value "nosniff"
match response header set "Referrer-Policy" \
value "strict-origin"
match response header set "Content-Security-Policy" \
value "style-src 'self' *.foobar.com \
https://fonts.googleapis.com; \
font-src 'self' data: *.foobar.com \
https://fonts.gstatic.com; \
default-src 'self' *.foobar.com"
match response header set "Feature-Policy" \
value "accelerometer 'none'; camera 'none'; \
geolocation 'none'; gyroscope 'none'; magnetometer \
'none'; microphone 'none'; payment 'none'; usb 'none'"
match response header set "Strict-Transport-Security" \
value "max-age=31536000"
### TCP performance options
tcp { nodelay, sack, socket buffer 65536, backlog 100 }
### Return HTTP/HTML error pages
return error
match request quick header "Host" value "cdn.foobar.com" \
forward to <cdn_hosts>
match request quick header "Host" value "foobar.com" \
forward to <app_hosts>
}
relay "proxy" {
listen on $relayd_addr port $plaintext_port
protocol "httpfilter"
# main server table
forward to <web_hosts> port $web_port
}
relay "tlsproxy" {
listen on $relayd_addr port 443 tls
protocol "httpsfilter" # N.B. http/s/filter
# N.B. the order as such
forward to <tls_web_hosts> port $tls_web_port
# additional server tables used by custom rules
forward to <cdn_hosts> port $cdn_port check tcp interval 60
forward to <app_hosts> port $app_port
}
There’s quite a few changes in there since the last time we put a plaintext ‘relayd.conf’ together. But in a nutshell, we are just expanding our rules to include TLS and moving the web-app from the plaintext protocol to the secure one.
N.B. in the ‘httpsfilter’ protocol, you may want to add the following match-rule if you wanted all your subdomains to be forced (in the client) to be over TLS:
match response header set "Strict-Transport-Security" \
value "max-age=31536000; includeSubDomains"
In our instance, we actually didn’t want it this way, but our case may be exceptional.
More nuances
relayd
is limited in that it expects your
certificates to be in a specific location. As
well, it expects them to be named after the IP
addresses upon which it is listening. This is the
state of affairs as of OpenBSD 6.5. This may or
may not change in the future.
But we can get around this, using soft-links. Follow along with some care:
cd /etc/ssl
doas ln -s foobar.com.pem 0.0.0.0.pem
doas ln -s foobar.com.fullchain.pem 0.0.0.0.crt # N.B. not a typo
doas su
root> cd /etc/ssl/private
root> doas ln -s foobar.com.key 0.0.0.0.key
root> exit
With that, we are ready to put our new ‘relayd.conf’ into the front lines and start serving our web-app over TLS. We can do that as follows:
doas cp ~/relayd.conf.master /etc/relayd.conf
doas cp ~/httpd.conf.master /etc/httpd.conf
doas su
root> relayd -n
root> httpd -n
root> rcctl reload httpd
root> rcctl reload relayd
root> exit
And there you have it: ‘https.’ You can employ
curl
to make sure all your domains are
redirecting accordingly. For example:
...
curl -I -XGET https://foobar.com
curl -I http://foobar.com
...
You should see some 200s and 301s. But that’s just child’s play; you can do some deep dives with the following resources:
- https://securityheaders.com
- https://www.ssllabs.com/ssltest
These can help you troubleshoot and tighten things up where you see fit.
Steps for securing the future
A nice way to have your certificates updated for
you, is just let ‘cron’ take that on. Here’s a
line one should add to root’s crontab -e
:
15 3 * * * acme-client foobar.com && rcctl reload relayd
With that, your certificates are scheduled for updates. Easy.
One note of importance, if you’re adding a subdomain, or changing anything at all, it’s necessary to update certificates deterministically with the correct flags.
For example, say you wanted to add ‘www1.foobar.com’ to your project?
Here’s how it’s done.
Make the change to your ‘acme-client.conf’ in the
‘alternative names’ block. Then add the new alias
to your ‘httpd.conf,’ not forgetting to execute
doas rcctl reload httpd
so plaintext traffic
can reach your ‘amce’ challenge reply at ‘www1.’
After all this has been done, you’ll want to perform the following:
doas mv /etc/ssl/foobar.com.crt /etc/ssl/foobar.com.crt.bak
doas acme-client -vFAD foobar.com
If you don’t do this, errors will come looking for you. If it all went well, you are in the clear to remove the ‘foobar.com.crt.bak’ file.