- https://myapp1.com => OK
- https://www.myapp1.com/ => OK
- http://myapp1.com/ => OK
- http://www.myapp1.com/ => OK
- https://mystaticsite.com => OK
- https://www.mystaticsite.com/ => OK
- http://mystaticsite.com/ => OK
- http://www.mystaticsite.com/ => OK
Rated A+ at SSL Labs and Security Headers
A simpler and more secure alternative to Linux/NGiNX/Apache/Passenger for hosting static sites and Ruby/Python/Node type web apps on the same server at the same time. This example uses OpenBSD's relayd to direct traffic to OpenBSD's httpd for static sites, and Ruby on Rails/Puma for web apps.
Choose OpenBSD for your Unix needs. OpenBSD -- the world's simplest and most secure Unix-like OS. Creator of the NGiNX/Apache webserver replacement httpd, the OpenSSL crypto stack replacement LibreSSL, the iptables/nftables firewall replacement pf, the Postfix mail server replacement OpenSMTPD, OpenSSH and much, much more. OpenBSD -- the cleanest kernel, the cleanest userland and the cleanest configuration syntax.
Files in this setup:
/etc/relayd.conf
/etc/httpd.conf
/etc/acme-client.conf
/etc/rc.d/myapp1
/etc/rc.d/__helper
/home/myapp1/myapp1/config/puma.rb
/home/myapp1/myapp1/config/environments/production.rb
acme-client -v mystaticsite.com
acme-client -v myapp1.com
acme-client -v myapp2.com
chmod 750 /etc/ssl/private
crontab -e
0 * * * * sleep $((RANDOM \% 2048)) && acme-client mystaticsite.com && rcctl restart relayd
0 * * * * sleep $((RANDOM \% 2048)) && acme-client myapp1.com && rcctl restart myapp1
0 * * * * sleep $((RANDOM \% 2048)) && acme-client myapp2.com && rcctl restart myapp2
public="vio0"
private="lo0"
table <acme_challenge> { $private }
acme_challenge_port="5000"
table <httpd_https_redirect> { $private }
httpd_https_redirect_port="6000"
table <httpd_static_sites> { $private }
httpd_static_sites_port="7000"
table <rails1> { $private }
rails1_ssl_port="8000"
table <rails2> { $private }
rails2_ssl_port="8002"
# Protocols
http protocol "http" {
# ACME-compliant HTTP-01 challenge
# https://letsencrypt.org/docs/challenge-types/
pass request quick path "/.well-known/acme-challenge/*" forward to <acme_challenge>
# Filter out Rails apps by domain name
# https://unix.stackexchange.com/questions/466232/openbsd-relay-https-proxy-for-multiple-domains
pass request header "Host" value "myapp1.com" forward to <rails1>
pass request header "Host" value "www.myapp1.com" forward to <rails1>
pass request header "Host" value "myapp2.com" forward to <rails2>
pass request header "Host" value "www.myapp2.com" forward to <rails2>
}
http protocol "https_reverse_proxy" {
# Block all by default
block
# Start opening up
pass request header "Host" value "mystaticsite.com" forward to <httpd_static_sites>
pass request header "Host" value "www.mystaticsite.com" forward to <httpd_static_sites>
pass request header "Host" value "myapp1.com" forward to <rails1>
pass request header "Host" value "www.myapp1.com" forward to <rails1>
pass request header "Host" value "myapp2.com" forward to <rails2>
pass request header "Host" value "www.myapp2.com" forward to <rails2>
tls keypair "mystaticsite"
tls keypair "myapp1"
tls keypair "myapp2"
# Pass address headers
match header set "X-Client-IP" value "$REMOTE_ADDR:$REMOTE_PORT"
match header set "X-Forwarded-For" value "$REMOTE_ADDR"
match header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
# Best practice security headers
# https://securityheaders.com
# https://www.marksayson.com/blog/setting_http_security_headers_in_rails/
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 "Referrer-Policy" value "strict-origin"
match response header set "Content-Security-Policy" value "default-src 'self'; script-src 'self' www.google.com www.gstatic.com; style-src 'self' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; frame-src 'self' www.google.com;"
match response header set "Feature-Policy" value "accelerometer 'none'; ambient-light-sensor 'none'; battery 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; usb 'none';"
}
# Relays
relay "http" {
listen on $public port http
protocol "http"
forward to <httpd_https_redirect> port $httpd_https_redirect_port
# Assumes Rails HTTPS redirection is enabled
# https://api.rubyonrails.org/classes/ActionDispatch/SSL.html
forward to <rails1> port $rails1_ssl_port
forward to <rails2> port $rails2_ssl_port
forward to <acme_challenge> port $acme_challenge_port
}
relay "https" {
listen on $public port https tls
protocol "https_reverse_proxy"
forward with tls to <httpd_static_sites> port $httpd_static_sites_port
forward to <rails1> port $rails1_ssl_port
forward to <rails2> port $rails2_ssl_port
}
types {
include "/usr/share/misc/mime.types"
}
localhost="lo0"
acme_challenge_port="5000"
https_redirect_port="6000"
static_sites_port="7000"
# HTTP
server "*" {
listen on $localhost port $acme_challenge_port
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "*" {
block drop
}
}
server "mystaticsite.com" {
listen on $localhost port $https_redirect_port
block return 301 "https://mystaticsite.com$DOCUMENT_URI"
}
# HTTPS
server "mystaticsite.com" {
listen on * tls port $static_sites_port
root "/htdocs/mystaticsite"
tls {
certificate "/etc/ssl/mystaticsite.crt"
key "/etc/ssl/private/mystaticsite.key"
}
hsts {
preload
}
}
server "www.mystaticsite.com" {
listen on * tls port $static_sites_port
tls {
certificate "/etc/ssl/mystaticsite.crt"
key "/etc/ssl/private/mystaticsite.key"
}
block return 301 "https://mystaticsite.com$DOCUMENT_URI"
}
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/ssl/private/letsencrypt.key"
}
domain mystaticsite.com {
alternative names { www.mystaticsite.com }
domain key "/etc/ssl/private/mystaticsite.key"
domain full chain certificate "/etc/ssl/mystaticsite.crt"
sign with letsencrypt
}
domain myapp1.com {
alternative names { www.myapp1.com }
domain key "/etc/ssl/private/myapp1.key"
domain full chain certificate "/etc/ssl/myapp1.crt"
sign with letsencrypt
}
domain myapp2.com {
alternative names { www.myapp2.com }
domain key "/etc/ssl/private/myapp2.key"
domain full chain certificate "/etc/ssl/myapp2.crt"
sign with letsencrypt
}
#!/bin/sh
# http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/infrastructure/templates/rc.template
app="myapp1"
# Get full path to helper
# https://stackoverflow.com/questions/38978650/run-a-script-in-the-same-directory-as-the-current-script
script_name="$0"
script_full_path=$(dirname "$0")
daemon="$script_full_path/__helper"
rc_bg=YES
. /etc/rc.d/rc.subr
rc_start() {
${rcexec} "${daemon} start ${app}"
}
rc_check() {
${rcexec} "${daemon} status ${app}"
}
rc_restart() {
${rcexec} "${daemon} phased-restart ${app}"
}
rc_stop() {
${rcexec} "${daemon} stop ${app}"
}
rc_cmd $1
#!/bin/sh
doas -u $2 sh -c "cd /home/$2/$2 && bundle exec pumactl --config-file config/puma.rb $1" --
#!/usr/bin/env puma
app = "myapp1"
ssl_bind "127.0.0.1", "8000", {
key: "/etc/ssl/private/#{app}.key",
cert: "/etc/ssl/#{app}.crt"
}
bind "tcp://127.0.0.1:8001"
pidfile "/home/#{app}/#{app}/tmp/puma.pid"
state_path "/home/#{app}/#{app}/tmp/puma.state"
stdout_redirect "/home/#{app}/#{app}/log/puma_access.log", "/home/#{app}/#{app}/log/puma_errors.log"
environment "production"
Rails.application.configure do
[...]
# We won't be using NGiNX or Apache, so comment this out
# config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Force all access to the app over SSL, use Strict-Transport-Security and secure cookies
config.force_ssl = true
[...]
end
Based on:
- Enable HTTPS with acme-client(1) and Let’s Encrypt on OpenBSD
- httpd and relayd on OpenBSD
- StackOverflow: OpenBSD relay, https proxy for multiple domains
Huge thanks to Aaron D.P., David K., and all the awesome folks in Freenode #openbsd
and #ruby
.
Fantastic! Haven't tried 6.7 yet, but I hear it has TLS 1.3 in relayd which is very good for SEO!
I don't think that should be necessary. If instead you export
GEM_HOME
, then Bundler will know where to install the newest version. And ofcourse, no more need for--user-install
.Interesting, I'm reading now. It should be okay. But if root, it is best to install with
install.sh
for proper integrity checks.Let's keep in touch, later!